Skip to content

Commit 152f92d

Browse files
committed
security: use parameterized queries for ClickHouse model usage stats
1 parent 101399d commit 152f92d

File tree

1 file changed

+59
-40
lines changed

1 file changed

+59
-40
lines changed

valhalla/jawn/src/managers/ModelUsageStatsManager.ts

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,22 @@ const TIME_CONFIG = {
110110
} as const;
111111

112112
export class ModelUsageStatsManager {
113+
/**
114+
* Build parameterized IN clause placeholders and params for ClickHouse queries.
115+
* Returns placeholders like ({val_0:String}, {val_1:String}, ...) and the corresponding params array.
116+
*/
117+
private buildInClauseParams(
118+
values: string[],
119+
offset: number = 0
120+
): { placeholders: string; params: string[] } {
121+
if (values.length === 0) {
122+
return { placeholders: "('')", params: [] };
123+
}
124+
const placeholders = values
125+
.map((_, i) => `{val_${offset + i}:String}`)
126+
.join(", ");
127+
return { placeholders: `(${placeholders})`, params: [...values] };
128+
}
113129
/**
114130
* Get model usage: top 9 models + "other" with time series and leaderboard.
115131
*/
@@ -136,16 +152,15 @@ export class ModelUsageStatsManager {
136152

137153
const top9Data = top9Result.data ?? [];
138154
const top9Set = new Set(top9Data.map((r) => r.model));
139-
const inClause = top9Data.length > 0
140-
? top9Data.map((r) => `'${r.model}'`).join(",")
141-
: "''";
155+
const { placeholders: modelInClause, params: modelParams } =
156+
this.buildInClauseParams(top9Data.map((r) => r.model));
142157

143158
// Step 2: Get "other" total + previous period data + time series (parallel)
144159
const otherQuery = `
145160
SELECT sum(${TOTAL_TOKENS_EXPR}) as total_tokens
146161
FROM request_stats
147162
WHERE hour >= now() - ${interval} AND ${BASE_WHERE_CLAUSE}
148-
AND model NOT IN (${inClause})
163+
AND model NOT IN ${modelInClause}
149164
`;
150165

151166
const prevTop9Query = `
@@ -154,7 +169,7 @@ export class ModelUsageStatsManager {
154169
WHERE hour >= now() - ${interval} - ${interval}
155170
AND hour < now() - ${interval}
156171
AND ${BASE_WHERE_CLAUSE}
157-
AND model IN (${inClause})
172+
AND model IN ${modelInClause}
158173
GROUP BY model
159174
`;
160175

@@ -164,7 +179,7 @@ export class ModelUsageStatsManager {
164179
WHERE hour >= now() - ${interval} - ${interval}
165180
AND hour < now() - ${interval}
166181
AND ${BASE_WHERE_CLAUSE}
167-
AND model NOT IN (${inClause})
182+
AND model NOT IN ${modelInClause}
168183
`;
169184

170185
const timeSeriesQuery = `
@@ -179,9 +194,9 @@ export class ModelUsageStatsManager {
179194

180195
const [otherResult, prevTop9Result, prevOtherResult, timeSeriesResult] =
181196
await Promise.all([
182-
clickhouseDb.dbQuery<{ total_tokens: number }>(otherQuery, []),
183-
clickhouseDb.dbQuery<{ model: string; total_tokens: number }>(prevTop9Query, []),
184-
clickhouseDb.dbQuery<{ total_tokens: number }>(prevOtherQuery, []),
197+
clickhouseDb.dbQuery<{ total_tokens: number }>(otherQuery, modelParams),
198+
clickhouseDb.dbQuery<{ model: string; total_tokens: number }>(prevTop9Query, modelParams),
199+
clickhouseDb.dbQuery<{ total_tokens: number }>(prevOtherQuery, modelParams),
185200
clickhouseDb.dbQuery<{ time_bucket: string; model: string; total_tokens: number }>(timeSeriesQuery, []),
186201
]);
187202

@@ -424,15 +439,14 @@ export class ModelUsageStatsManager {
424439

425440
const top9Data = top9Result.data ?? [];
426441
const top9Set = new Set(top9Data.map((r) => r.provider));
427-
const inClause = top9Data.length > 0
428-
? top9Data.map((r) => `'${r.provider}'`).join(",")
429-
: "''";
442+
const { placeholders: providerInClause, params: providerParams } =
443+
this.buildInClauseParams(top9Data.map((r) => r.provider));
430444

431445
const otherQuery = `
432446
SELECT sum(${TOTAL_TOKENS_EXPR}) as total_tokens
433447
FROM request_stats
434448
WHERE hour >= now() - ${interval} AND ${BASE_WHERE_CLAUSE}
435-
AND provider NOT IN (${inClause})
449+
AND provider NOT IN ${providerInClause}
436450
`;
437451

438452
const prevTop9Query = `
@@ -441,7 +455,7 @@ export class ModelUsageStatsManager {
441455
WHERE hour >= now() - ${interval} - ${interval}
442456
AND hour < now() - ${interval}
443457
AND ${BASE_WHERE_CLAUSE}
444-
AND provider IN (${inClause})
458+
AND provider IN ${providerInClause}
445459
GROUP BY provider
446460
`;
447461

@@ -451,7 +465,7 @@ export class ModelUsageStatsManager {
451465
WHERE hour >= now() - ${interval} - ${interval}
452466
AND hour < now() - ${interval}
453467
AND ${BASE_WHERE_CLAUSE}
454-
AND provider NOT IN (${inClause})
468+
AND provider NOT IN ${providerInClause}
455469
`;
456470

457471
const timeSeriesQuery = `
@@ -466,9 +480,9 @@ export class ModelUsageStatsManager {
466480

467481
const [otherResult, prevTop9Result, prevOtherResult, timeSeriesResult] =
468482
await Promise.all([
469-
clickhouseDb.dbQuery<{ total_tokens: number }>(otherQuery, []),
470-
clickhouseDb.dbQuery<{ provider: string; total_tokens: number }>(prevTop9Query, []),
471-
clickhouseDb.dbQuery<{ total_tokens: number }>(prevOtherQuery, []),
483+
clickhouseDb.dbQuery<{ total_tokens: number }>(otherQuery, providerParams),
484+
clickhouseDb.dbQuery<{ provider: string; total_tokens: number }>(prevTop9Query, providerParams),
485+
clickhouseDb.dbQuery<{ total_tokens: number }>(prevOtherQuery, providerParams),
472486
clickhouseDb.dbQuery<{ time_bucket: string; provider: string; total_tokens: number }>(timeSeriesQuery, []),
473487
]);
474488

@@ -672,13 +686,15 @@ export class ModelUsageStatsManager {
672686
timeframe: StatsTimeFrame
673687
): Promise<Result<ProviderStatsResponse, string>> {
674688
const { interval, bucket } = TIME_CONFIG[timeframe];
675-
const escapedProvider = provider.replace(/'/g, "''");
689+
690+
// Provider param is val_0
691+
const providerPlaceholder = `{val_0:String}`;
676692

677693
const top9Query = `
678694
SELECT model, sum(${TOTAL_TOKENS_EXPR}) as total_tokens
679695
FROM request_stats
680696
WHERE hour >= now() - ${interval} AND ${BASE_WHERE_CLAUSE}
681-
AND provider = '${escapedProvider}'
697+
AND provider = ${providerPlaceholder}
682698
GROUP BY model
683699
ORDER BY total_tokens DESC
684700
LIMIT 9
@@ -687,7 +703,7 @@ export class ModelUsageStatsManager {
687703
const top9Result = await clickhouseDb.dbQuery<{
688704
model: string;
689705
total_tokens: number;
690-
}>(top9Query, []);
706+
}>(top9Query, [provider]);
691707
if (top9Result.error) return err(top9Result.error);
692708

693709
const top9Data = top9Result.data ?? [];
@@ -701,21 +717,24 @@ export class ModelUsageStatsManager {
701717
}
702718

703719
const top9Set = new Set(top9Data.map((r) => r.model));
704-
const inClause = top9Data.map((r) => `'${r.model}'`).join(",");
720+
// Model params start at offset 1 (provider is val_0)
721+
const { placeholders: modelInClause, params: modelParams } =
722+
this.buildInClauseParams(top9Data.map((r) => r.model), 1);
723+
const allParams = [provider, ...modelParams];
705724

706725
const otherQuery = `
707726
SELECT sum(${TOTAL_TOKENS_EXPR}) as total_tokens
708727
FROM request_stats
709728
WHERE hour >= now() - ${interval} AND ${BASE_WHERE_CLAUSE}
710-
AND provider = '${escapedProvider}'
711-
AND model NOT IN (${inClause})
729+
AND provider = ${providerPlaceholder}
730+
AND model NOT IN ${modelInClause}
712731
`;
713732

714733
const totalQuery = `
715734
SELECT sum(${TOTAL_TOKENS_EXPR}) as total_tokens
716735
FROM request_stats
717736
WHERE hour >= now() - ${interval} AND ${BASE_WHERE_CLAUSE}
718-
AND provider = '${escapedProvider}'
737+
AND provider = ${providerPlaceholder}
719738
`;
720739

721740
const prevTop9Query = `
@@ -724,8 +743,8 @@ export class ModelUsageStatsManager {
724743
WHERE hour >= now() - ${interval} - ${interval}
725744
AND hour < now() - ${interval}
726745
AND ${BASE_WHERE_CLAUSE}
727-
AND provider = '${escapedProvider}'
728-
AND model IN (${inClause})
746+
AND provider = ${providerPlaceholder}
747+
AND model IN ${modelInClause}
729748
GROUP BY model
730749
`;
731750

@@ -735,8 +754,8 @@ export class ModelUsageStatsManager {
735754
WHERE hour >= now() - ${interval} - ${interval}
736755
AND hour < now() - ${interval}
737756
AND ${BASE_WHERE_CLAUSE}
738-
AND provider = '${escapedProvider}'
739-
AND model NOT IN (${inClause})
757+
AND provider = ${providerPlaceholder}
758+
AND model NOT IN ${modelInClause}
740759
`;
741760

742761
const timeSeriesQuery = `
@@ -746,17 +765,17 @@ export class ModelUsageStatsManager {
746765
sum(${TOTAL_TOKENS_EXPR}) as total_tokens
747766
FROM request_stats
748767
WHERE hour >= now() - ${interval} AND ${BASE_WHERE_CLAUSE}
749-
AND provider = '${escapedProvider}'
768+
AND provider = ${providerPlaceholder}
750769
GROUP BY time_bucket, model
751770
`;
752771

753772
const [otherResult, totalResult, prevTop9Result, prevOtherResult, timeSeriesResult] =
754773
await Promise.all([
755-
clickhouseDb.dbQuery<{ total_tokens: number }>(otherQuery, []),
756-
clickhouseDb.dbQuery<{ total_tokens: number }>(totalQuery, []),
757-
clickhouseDb.dbQuery<{ model: string; total_tokens: number }>(prevTop9Query, []),
758-
clickhouseDb.dbQuery<{ total_tokens: number }>(prevOtherQuery, []),
759-
clickhouseDb.dbQuery<{ time_bucket: string; model: string; total_tokens: number }>(timeSeriesQuery, []),
774+
clickhouseDb.dbQuery<{ total_tokens: number }>(otherQuery, allParams),
775+
clickhouseDb.dbQuery<{ total_tokens: number }>(totalQuery, [provider]),
776+
clickhouseDb.dbQuery<{ model: string; total_tokens: number }>(prevTop9Query, allParams),
777+
clickhouseDb.dbQuery<{ total_tokens: number }>(prevOtherQuery, allParams),
778+
clickhouseDb.dbQuery<{ time_bucket: string; model: string; total_tokens: number }>(timeSeriesQuery, [provider]),
760779
]);
761780

762781
if (otherResult.error) return err(otherResult.error);
@@ -833,13 +852,13 @@ export class ModelUsageStatsManager {
833852
timeframe: StatsTimeFrame
834853
): Promise<Result<ModelStatsResponse, string>> {
835854
const { interval, bucket } = TIME_CONFIG[timeframe];
836-
const escapedModel = model.replace(/'/g, "''");
855+
const modelPlaceholder = `{val_0:String}`;
837856

838857
const totalQuery = `
839858
SELECT sum(${TOTAL_TOKENS_EXPR}) as total_tokens
840859
FROM request_stats
841860
WHERE hour >= now() - ${interval} AND ${BASE_WHERE_CLAUSE}
842-
AND model = '${escapedModel}'
861+
AND model = ${modelPlaceholder}
843862
`;
844863

845864
const timeSeriesQuery = `
@@ -848,14 +867,14 @@ export class ModelUsageStatsManager {
848867
sum(${TOTAL_TOKENS_EXPR}) as total_tokens
849868
FROM request_stats
850869
WHERE hour >= now() - ${interval} AND ${BASE_WHERE_CLAUSE}
851-
AND model = '${escapedModel}'
870+
AND model = ${modelPlaceholder}
852871
GROUP BY time_bucket
853872
ORDER BY time_bucket
854873
`;
855874

856875
const [totalResult, timeSeriesResult] = await Promise.all([
857-
clickhouseDb.dbQuery<{ total_tokens: number }>(totalQuery, []),
858-
clickhouseDb.dbQuery<{ time_bucket: string; total_tokens: number }>(timeSeriesQuery, []),
876+
clickhouseDb.dbQuery<{ total_tokens: number }>(totalQuery, [model]),
877+
clickhouseDb.dbQuery<{ time_bucket: string; total_tokens: number }>(timeSeriesQuery, [model]),
859878
]);
860879

861880
if (totalResult.error) return err(totalResult.error);

0 commit comments

Comments
 (0)