All vulnerabilities
GHSA-pc3q-gw7f-p2x2

OpenPanel ClickHouse SQL injection via `event.events`

openpanel
CVSS
n/aHigh
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed inNone listedNo source names a fixed version yet
Published11 Oct 2026Updated 11 Oct 2026GitHub

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

  1. Register two users and create a project for each (auth.signUpEmail, then

onboarding.project with website:false, app:true), noting each projectId.

  1. 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').

  1. 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