{"schema":"threatline.record/2","record":{"id":"GHSA-pc3q-gw7f-p2x2","aliases":["GHSA-pc3q-gw7f-p2x2"],"summary":"OpenPanel ClickHouse SQL injection via `event.events`","details":"### Summary\r\n 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\r\nthe `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**.\r\n\r\n### Details\r\nThe 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.\r\n\r\n\r\n### PoC\r\n1. Register two users and create a project for each (`auth.signUpEmail`, then\r\n   `onboarding.project` with `website:false, app:true`), noting each `projectId`.\r\n2. Seed a victim event into ClickHouse for the victim project:\r\n   `INSERT INTO openpanel.events (name, project_id, profile_id, created_at, path) VALUES ('VICTIM_SECRET_EVENT_x','<victimProjectId>','vprofile',now64(3),'/x')`.\r\n3. As the attacker, call (URL-encode the input):\r\n   `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\"}]}}`\r\n   --> the response contains the victim project's event.\r\n\r\n\r\n\r\n### Impact\r\n\r\nAny 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.\r\n","categories":["Unclassified"],"product":"openpanel","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/Openpanel-dev/openpanel/security/advisories/GHSA-pc3q-gw7f-p2x2","source_modified_at":"2026-10-11T19:18:24Z","fetched_at":"2026-10-27T21:14:11.489891Z","package":{"ecosystem":"openpanel-dev","name":"openpanel"},"range":"<= f8bb75744d0e933dad548c58306adfc27aaf64a6"}],"fixed_versions":[],"cvss":null,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-11T19:18:24Z","modified_at":"2026-10-11T19:18:24Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/Openpanel-dev/openpanel/security/advisories/GHSA-pc3q-gw7f-p2x2","fetched_at":"2026-10-27T21:14:11.489891Z","source_modified_at":"2026-10-11T19:18:24Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-11T19:18:24Z","content_hash":"80d89a061411fe7ca1662a060b75cc03eb5b7d97836c2304222d9f3fa1c7c3e1"}}
