{"schema":"threatline.record/2","record":{"id":"GHSA-gvwr-5684-wjqc","aliases":["GHSA-gvwr-5684-wjqc"],"summary":"ClickHouse SQL injection via unescaped profile-filter column identifier in cohort query (cohort.service.ts profileColumnAccess)","details":"## Summary\r\n`profileColumnAccess(name)` (`@openpanel/db` `cohort.service.ts`) escapes only the `profiles.properties.<key>` branch; any other `name` is returned verbatim as a column identifier with NO allowlist, then spliced unescaped into `argMax(...)`, `cityHash64(...)` and comparison clauses. A project-read member reaches it via `cohort.preview`, injecting attacker-authored SQL in identifier context. This is a DISTINCT sink from GHSA-hwf2-2v2h-7892, whose fixed function `profileColumnSql` enforces the `PROFILE_COLUMNS` allowlist.\r\n\r\n## Root Cause\r\n`cohort.service.ts:308-317`: for a non-`properties` name, `profileColumnAccess` does `return normalizedName` verbatim. It is spliced into `argMax(${columnAccess}, …)` (:462), `cityHash64(${referencedColumns.join(', ')})` (:338) and comparison clauses (:468/478/523/527/532). The filter VALUE is `sqlstring.escape`d but the COLUMN identifier is not. GHSA-hwf2-2v2h-7892's fixed function `filter-where.service.ts:188 profileColumnSql` enforces `if (PROFILE_COLUMNS.has(withoutPrefix)) return withoutPrefix; return null;` — this cohort function has no equivalent.\r\n\r\n## Impact\r\nAttacker-authored SQL expression / correlated subquery in identifier context. A subquery the attacker writes in the HAVING/argMax position is not constrained by the outer `project_id` scope → cross-tenant ClickHouse read.\r\n\r\n## Proof of Concept\r\nCohort preview with a property filter:\r\n```\r\n{ name: \"id, (SELECT groupArray(email) FROM profiles WHERE project_id='victim')\", operator: \"isNotNull\", value: [] }\r\n```\r\n`isNotNull` with empty value skips the empty-value early return and emits `(${columnAccess} IS NOT NULL AND ${columnAccess} != '')` — injection needs no filter value.\r\n\r\n## Attack Chain\r\n1. Entry: tRPC `cohort.preview` (cohort.ts:210, protectedProcedure, query). Guard: `enforceAccess` requires READ on top-level projectId (trpc.ts:96-111, needsWrite=false for query); `zChartEventFilter.name = z.string()`. Bypass proof: any project-read member; name unconstrained, no allowlist, no `isKnownEventField` filter applied to cohort property names.\r\n2. Flow: `preview → countCohort/computeCohort → computePropertyBasedCohort (:560) → buildPropertyBasedCohortQuery (:352) → chQuery (:569)`.\r\n3. Check: identifier escaping/allowlist. Guard: none in `profileColumnAccess`. Bypass proof: returns `normalizedName` raw (cohort.service.ts:316); value escaped, column NOT.\r\n4. Sink: `chQuery(buildPropertyBasedCohortQuery(...))` (:569).\r\n5. Impact: cross-tenant ClickHouse read via attacker subquery in argMax/cityHash64/HAVING (outer project_id WHERE does not constrain it).\r\n\r\n## Bypass Evidence\r\nHEAD read confirms `profileColumnAccess` returns the name verbatim for the non-`properties` branch with no allowlist, spliced unescaped into identifier positions. Distinct function/file/entry from GHSA-hwf2-2v2h-7892's fixed `profileColumnSql` (which HAS the `PROFILE_COLUMNS` allowlist on HEAD).\r\n\r\n## Affected Versions\r\nRolling deploy; vulnerable on HEAD `61a9245`; fix unreleased.\r\n\r\n## Suggested Fix\r\nApply the same `PROFILE_COLUMNS` allowlist used by `profileColumnSql` to `profileColumnAccess`, rejecting any non-allowlisted column identifier.\r\n\r\n---\r\nReported by **zx (Jace)** — GitHub: @manus-use","categories":["Unclassified"],"product":"openpanel","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/Openpanel-dev/openpanel/security/advisories/GHSA-gvwr-5684-wjqc","source_modified_at":"2026-10-11T19:44:36Z","fetched_at":"2026-10-27T04:51:10.072820Z","package":{"ecosystem":"openpanel-dev","name":"openpanel"},"range":"<= 42fc8623b5c5c30f0b89686451c0c5ab13046b81"}],"fixed_versions":[],"cvss":6.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-11T19:44:36Z","modified_at":"2026-10-11T19:44:36Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/Openpanel-dev/openpanel/security/advisories/GHSA-gvwr-5684-wjqc","fetched_at":"2026-10-27T04:51:10.072820Z","source_modified_at":"2026-10-11T19:44:36Z"}],"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:44:36Z","content_hash":"313116f8c869e6710a239af6552f0c310f16ceb8699c6a312186c3925f18a89b"}}
