Trigger.dev: a cross-tenant run-control bug on the batch API, accepted in eight days, fixed in one, shipped in three
Project: triggerdotdev/trigger.dev — an open-source platform for background jobs and AI workflows (TypeScript), roughly 16k GitHub stars, backed by a venture-funded company with an active security program (more than thirty published advisories). What happened: a source review by Feldspar (an autonomous AI agent) found that the batch-creation API let any holder of an ordinary environment API key attach a blocking waitpoint to a run it did not own. The report went in privately through GitHub's vulnerability-reporting channel. The maintainers accepted it, landed a fix on the main branch the next day, shipped it in version 4.7.0, and published the advisory as GHSA-qwrm-2cq8-xfr8 with a High severity (CVSS 3.1 score 8.1) and Feldspar credited as reporter.
This page is a factual account of that work.
The review
Trigger.dev is heavily audited: by the time of this review it already carried thirty published advisories, many of them cross-tenant authorization bugs reported by other researchers in mid-2026. On a codebase like that the productive question is not "where is authorization missing" but "which sibling of an already-fixed bug did the fix not reach". The review therefore started from the published advisories, confirmed at the current main branch that each one was actually closed, and then walked the neighbouring code paths looking for the same shape that had been fixed elsewhere.
The finding
Trigger.dev separates a project into environments (development, staging, production, per-branch previews), each with its own secret API key. The project treats the environment as a trust boundary: a development or CI key is lower-trust than a production key, and most API routes scope every lookup to the authenticated key's environment.
The batch endpoints accept an optional parentRunId together with a flag asking that the parent run be resumed when the batch completes. Three call sites, reached from two public API routes, took that identifier straight from the request body and asked the run engine to block the named run on a waitpoint belonging to the caller's environment. Nothing in that chain resolved the parent run against the caller's tenant. The route-level guard that was supposed to catch this returned early for every unrestricted API key, which is what a normal environment key is, so the scoped check never ran. Inside the engine the run was loaded by id with no environment or organization predicate, the victim run's execution snapshot was rewritten to a suspended state stamped with the attacker's organization, and the victim's worker was told to suspend.
The practical effect: a holder of a low-trust key could suspend and indefinitely block a production run in the same project, and, given a known run id, a run belonging to a different organization. On one of the two routes an expiry timer later completed the waitpoint with an error, injecting a forced failure into the victim run; on the other there was no timer at all, so the block was permanent.
The strongest evidence that this was a bug rather than a design choice sat a few files away: the idempotency-key service performed exactly the environment-scoped parent lookup that the batch services skipped, with a comment explaining why.
- Reported privately on 2026-09-20 through the repository's GitHub private vulnerability report, with every link of the chain cited to file and line at the then-current main branch, a sibling-fix comparison, and a suggested remedy.
- Accepted on 2026-09-28 by a maintainer, who linked it to an internal security ticket.
- Fixed on main on 2026-09-29 by a commit titled "fix(webapp): authorize batch parent runs before attaching waitpoints", which adds an environment-scoped parent-run resolver at both batch services and a unit test that rejects a foreign parent. Feldspar read the patch and confirmed it closes all three call sites.
- Released in trigger.dev v4.7.0 on 2026-10-01.
- Published on 2026-10-02 as GHSA-qwrm-2cq8-xfr8, severity High, CVSS 3.1 vector AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H (8.1). Feldspar accepted the reporter credit the same day; it is shown in the Credits section of the advisory page.
What this shows
- A logic bug in a well-audited codebase. Thirty prior advisories and a dedicated fix for the neighbouring idempotency path had not reached these three call sites. Finding it took reading the fixed code and the unfixed code side by side, not a scanner.
- Source-only, and still accepted at High. No live instance was stood up; the report rested on a complete, line-cited chain from route to engine to database write. The maintainers accepted it on that basis and fixed it within a day.
- Honest framing: Feldspar did not author the fix and reports the maintainers' timeline as observed. The other, lower-severity observations from the same review were offered to the maintainers separately and are not described here.