Skip to content

Commit 134f1dc

Browse files
authored
[HDX-3277] Fix service filter quote escaping on Services page (#1931)
## Summary - escape service name values when generating the Services page SQL filter to prevent malformed queries when names contain quotes - switch from string interpolation to `SqlString.format` with a raw left-hand expression and escaped right-hand value ## Why - service names containing apostrophes/single quotes broke ClickHouse query parsing, causing the Services page to error Linear: https://linear.app/clickhouse/issue/HDX-3277/service-page-quote-escape-bug
1 parent 2b53b8e commit 134f1dc

3 files changed

Lines changed: 44 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperdx/app': patch
3+
---
4+
5+
fix: escape service filter values on Services page to handle quoted names safely

packages/app/src/ServicesDashboardPage.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
useQueryStates,
99
} from 'nuqs';
1010
import { UseControllerProps, useForm, useWatch } from 'react-hook-form';
11+
import SqlString from 'sqlstring';
1112
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
1213
import { convertDateRangeToGranularityString } from '@hyperdx/common-utils/dist/core/utils';
1314
import {
@@ -90,6 +91,13 @@ type AppliedConfig = AppliedConfigParams & {
9091

9192
const MAX_NUM_SERIES = HARD_LINES_LIMIT;
9293

94+
export function buildInFilterCondition(
95+
columnExpression: string,
96+
value: string,
97+
): string {
98+
return SqlString.format('? IN (?)', [SqlString.raw(columnExpression), value]);
99+
}
100+
93101
function getScopedFilters({
94102
appliedConfig,
95103
expressions,
@@ -112,7 +120,10 @@ function getScopedFilters({
112120
if (appliedConfig.service) {
113121
filters.push({
114122
type: 'sql',
115-
condition: `${expressions.service} IN ('${appliedConfig.service}')`,
123+
condition: buildInFilterCondition(
124+
expressions.service,
125+
appliedConfig.service,
126+
),
116127
});
117128
}
118129
if (includeNonEmptyEndpointFilter) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { buildInFilterCondition } from '../ServicesDashboardPage';
2+
3+
describe('buildInFilterCondition', () => {
4+
it.each([
5+
{
6+
columnExpression: 'ServiceName',
7+
value: 'checkout-service',
8+
expected: "ServiceName IN ('checkout-service')",
9+
},
10+
{
11+
columnExpression: "SpanAttributes['service.name']",
12+
value: "O'Reilly API",
13+
expected: "SpanAttributes['service.name'] IN ('O\\'Reilly API')",
14+
},
15+
{
16+
columnExpression: "ResourceAttributes['service.namespace']",
17+
value: 'payments "v2"',
18+
expected:
19+
"ResourceAttributes['service.namespace'] IN ('payments \\\"v2\\\"')",
20+
},
21+
])(
22+
'escapes value and keeps column expression for $columnExpression',
23+
({ columnExpression, value, expected }) => {
24+
expect(buildInFilterCondition(columnExpression, value)).toBe(expected);
25+
},
26+
);
27+
});

0 commit comments

Comments
 (0)