OpenPanel ClickHouse SQL injection via `event.events`
What fixes it
No source in this snapshot names a fixed version.
Description
Summary
the tRPC event.events query splices a profile.* filter name (zChartEventFilter.name, unconstrained z.string()) raw into the profiles-JOIN subquery of the event-list query (packages/db/src/services/event.service.ts:699). Because JOINs are emitted before the WHERE clause on a single line, a trailing -- inside the injected JOIN comments out the outer WHERE project_id = '<caller>' scope, so the query returns events from all projects / all tenants.
Details
The JOIN-construction path builds an identifier/column list by string-concatenating user-controlled filter names with no escaping and no allowlist. Elsewhere the bare-column WHERE branch allowlists against EVENT_TOP_LEVEL_COLUMNS, but that guard is not applied to the JOIN column list; split('.')[0] is a truncation, not a sanitizer.
PoC
- Register two users and create a project for each (
auth.signUpEmail, then
onboarding.project with website:false, app:true), noting each projectId.
- Seed a victim event into ClickHouse for the victim project:
INSERT INTO openpanel.events (name, project_id, profile_id, created_at, path) VALUES ('VICTIM_SECRET_EVENT_x','<victimProjectId>','vprofile',now64(3),'/x').
- As the attacker, call (URL-encode the input):
GET /trpc/event.events?input={"json":{"projectId":"<attackerProjectId>","filters":[{"name":"profile.email, project_id FROM profiles) AS p USING (project_id) -- ","operator":"is","value":["x"],"type":"string"}]}} --> the response contains the victim project's event.
Impact
Any authenticated user with read on one project can dump events (path, country, city, profile_id, revenue, etc.) from ALL other projects/tenants. zChartEventFilter.name is unconstrained z.string(); the fragment after profile. up to the first '.' is inserted verbatim into the JOIN subquery's SELECT column list. The '.'-truncation still permits spaces, parentheses, quotes, ClickHouse USING(...) join syntax and -- comments - enough to close the subquery and comment out the trailing WHERE project_id scoping so the outer SELECT ... FROM events e returns rows for every project. Results are returned directly.
Text from GitHub Advisory Database
Affected versions 1
- openpanel, openpanel-dev<= f8bb75744d0e933dad548c58306adfc27aaf64a6GitHub