Escape property keys in ClickHouse Map access expressions - #483
Conversation
A property key arrives as free text (a filter name, a breakdown name, a math-metric property) and was concatenated between literal quotes, so a key containing a quote stopped parsing as one Map access and its tail was read as SQL, right beside the project_id predicate that scopes the query. Route the key through sqlstring.escape, the way every other Map render in the file already does, and build the profile-property narrowing search string from the same helper so a narrowed key keeps matching the CTE column that replaces it. WHERE clauses are now parenthesised before they are AND-joined, so a clause holding a top-level OR cannot change how its neighbours group. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 8 included reviews per hour; 2 remain after this review. 📝 WalkthroughWalkthroughThe change escapes property keys in ClickHouse Map-access expressions, centralizes profile-property references, parenthesizes individual ChangesSQL safety changes
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This change keeps quoted and backslash-containing property keys inside SQL Map literals and preserves predicate grouping, with coverage for affected chart, event, profile, and funnel queries. No merge-blocking risk remains. Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Thanks for flagging this — the property-key handling that let a query cross a project's boundary is fixed. That's in PR #483, which just merged and will go out with the next release, probably within a day or so. |
What changed
A property path (
properties.foo,profile.properties.plan) reaches the SQL builders as free text: a filter name, a breakdown name, or a math-metric property, all of which come from a saved report or an API call.getSelectPropertyKeyturned that path into a Map access by concatenating the key between literal quotes:A key containing a single quote therefore stopped being one Map access. It closed the string literal, and the remainder of the key was parsed as SQL, sitting in the same WHERE list as the
project_idpredicate that scopes the query to one project. A key likex'] = '' OR 1 = 1 OR properties['yproduced a valid query with an extra boolean operator in it.Three changes:
getSelectPropertyKeybuilds the non-wildcard Map access withsqlstring.escape, the same way the wildcard branch and the group/profile helpers next to it already do. Ordinary keys render byte-for-byte as before; keys with a quote or backslash now stay inside the literal.rewriteProfilePropertyRefssearches for the reference text through a sharedprofilePropertyRefhelper, so it keeps matching whatgetSelectPropertyKeyemits. Without this, a narrowed key with a quote would have kept a reference to a Map the profile CTE no longer selects.getWherein the SQL builder parenthesises each clause before joining withAND. Previously a clause holding a top-levelORchanged how its neighbours grouped.Evidence
packages/db/src/services/chart.service.ts:382— the unescaped render (before this change).packages/db/src/services/chart.service.ts:260,:280,:314,:377— every other Map render in the same file goes throughsqlstring.escape; so does the filter side atpackages/db/src/services/filter-where.service.ts:192and:211.packages/db/src/services/chart.service.ts:756and:1098, math metrics at:786and:1135, and filters at:1189(getEventFiltersWhereClause), which the event list and event count queries use atpackages/db/src/services/event.service.ts:682and:766.packages/db/src/services/chart.service.ts:445—rewriteProfilePropertyRefsmatched the exact textprofile.properties['<key>'].packages/db/src/sql-builder.ts:36—getWherejoined clauses withANDand no parentheses.Tests
New
packages/db/src/services/property-key-escaping.test.ts(string assertions only, no ClickHouse needed):getSelectPropertyKeyrenders a key containing a quote, a backslash and a]as one escaped literal, and renders ordinary keys (properties.foo, thee.-qualified form,profile.properties.plan, theproperties.a.*wildcard, plain columns) exactly as before.project_id =predicate and the payload stays inside the key literal. Same assertion for the event list and event count queries, captured through a mockedchQuery.getWhereparenthesises its clauses.All ten of the injection assertions fail on the current code and pass with the change.
One existing assertion moved:
packages/db/src/services/funnel-sql.test.tspinned the funnel step pre-filter text, which now carries the extra parentheses fromgetWhere. The SQL means the same thing; the neighbouring assertion that counts each complete step condition twice is untouched.Deliberately left out
getAggregateChartSqlandgetChartSqlbuild theone_event_per_usersubquery withjoin(sb.where, ' AND ')directly (chart.service.ts:806,:1155), which still has no parentheses. No builder emits an unparenthesised top-levelORtoday:contains,regex,isNulland the group branch all wrap their own clauses. Changing those two call sites is a wider edit than this needed.transformPropertyKey's non-wildcard return (chart.service.ts:245) still concatenates quotes, but its only caller reaches it on the wildcard path and wraps the result insqlstring.escape. Left alone.biome checkreports pre-existing complaints in these files (12 inchart.service.ts, 2 insql-builder.tsbefore and after this change); the new test file follows the style of its neighbours rather than the formatter's output.Checks
vitest runoverpackages/db/src/services— 168 passed, 25 skipped.retention.service.test.tsfails in this environment because it needs a reachable ClickHouse; it fails identically without this change.tsc --noEmitinpackages/db— 26 errors, all pre-existing, innotification.service.ts,organization.service.tsandinsights/store.ts. None in the touched files.🤖 Generated with Claude Code
Summary by CodeRabbit
ORdo not broaden results unexpectedly.