Patch Forecast
Yellow warning
Plan into this sprint.

GHSA-qxpp-qjg8-x4jv

Trigger.dev: Run replay injects a task run into an attacker-chosen environment (cross-tenant write)

Risk score
48/100
CVSS
7.1 v3.1
Published
2 Oct 2026

Open in the live forecast Today's forecast

What to know

What it is
The dashboard replay action authorizes the source run (it must belong to the caller's org), but the target environment for the replayed run is taken verbatim from the request body and is never checked for org or project membership. The environment lookup used by the replay path filters by id only. As a result, an auth…
Affected
trigger.dev (npm) < 4.5.2
Fix
Upgrade trigger.dev to 4.5.2 or later.
Exploitation
No known exploitation

Story map

Story map: trigger.devTHE FLAWSTHE FIXGHSA-qxpp-qjg8-x4jvTrigger.dev: Run replay injectsa task run into anattacker-chosen environment…CVSS 7.1GHSA-gg6r-gp4c-89hpTrigger.dev: V1 coordinatordefault-secret unauthSocket.IOCVSS 9.2GHSA-pqxw-g93w-hj9xTrigger.dev Self-HostedDeployment: Default Secretsallow Unauthenticated…CVSS 8.1GHSA-9q4r-4842-93vwTrigger.dev: Cross-tenant SQLinjection in the TSQL querycompiler (POST /api/v1/query)…CVSS 7.7Upgrade trigger.dev4.5.4or later on your release lineWho is exposedtrigger.dev (npm) < 4.5.4AMBER · PATCH THIS WEEKtrigger.devThe /coordinator Socket.IOnamespace mounts on everywebapp boot and authenticateswith a default secret("coordinator-secret") bakedinto source. The overridevariable isn't documented in the…Where it standsNo known exploitationCONNECTS TOSame weaknessAuthorisation bypass (CWE-639)245 other warnings in 30 days →
Story map: the flaws, the fix, how it unfolded and what it connects to. Boxes link to the record or article.

At a glance

trigger.dev: Amber, patch this week. The /coordinator Socket.IO namespace mounts on every webapp boot and authenticates with a default secret ("coordinator-secret") baked into source. The override variable isn't documented in the self-host docs, .env.example, or helm values, so any operator who didn't read source ships with the default. Once connected, R…

The flaws

  • GHSA-qxpp-qjg8-x4jv: Trigger.dev: Run replay injects a task run into an attacker-chosen environment (cross-tenant write) (CVSS 7.1)
  • GHSA-gg6r-gp4c-89hp: Trigger.dev: V1 coordinator default-secret unauth Socket.IO (CVSS 9.2)
  • GHSA-pqxw-g93w-hj9x: Trigger.dev Self-Hosted Deployment: Default Secrets allow Unauthenticated Infrastructure Compromise (CVSS 8.1)
  • GHSA-9q4r-4842-93vw: Trigger.dev: Cross-tenant SQL injection in the TSQL query compiler (POST /api/v1/query) via unsanitized window-function name (CVSS 7.7)

The fix

Upgrade trigger.dev: 4.5.4 or later on your release line

Who is exposed: trigger.dev (npm) < 4.5.4

Where it stands

  • No known exploitation

Connects to

Why it is Yellow

  • CVSS 7.1 high
  • On your watchlist: npm

Affected packages

PackageEcosystemVulnerableFixed in
trigger.devnpm< 4.5.24.5.2

References

Original description

### Summary The dashboard replay action authorizes the source run (it must belong to the caller's org), but the target environment for the replayed run is taken verbatim from the request body and is never checked for org or project membership. The environment lookup used by the replay path filters by id only. As a result, an authenticated user can replay one of their own runs into another organization's or project's environment, creating a task run there that consumes the victim tenant's queue and compute and pollutes their run history. ### Details The replay action member-scopes the source run correctly (apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts, the findFirst on taskRun filtered by `project.organization.members.some.userId`), but passes the target environment straight from the submitted form, resources.taskruns.$runParam.replay.ts:344-346: ```js const replayRunService = new ReplayTaskRunService(); const newRun = await replayRunService.call(taskRun, { environmentId: submission.value.environment, // attacker-controlled, unvalidated payload: submission.value.payload, ... }); ``` `submission.value.environment` comes from ReplayRunData (apps/webapp/app/v3/replayTask.ts: `environment: z.string().optional()`), so it is fully client-controlled. ReplayTaskRunService resolves it with no ownership check, apps/webapp/app/v3/services/replayTaskRun.server.ts:26-28: ```js const authenticatedEnvironment = await findEnvironmentById( overrideOptions.environmentId ?? existingTaskRun.runtimeEnvironmentId ); ``` findEnvironmentById, apps/webapp/app/models/runtimeEnvironment.server.ts:197-211: ```js export async function findEnvironmentById(id) { const environment = await $replica.runtimeEnvironment.findFirst({ where: { id }, // no membership / org / project filter include: authIncludeWithParent, }); if (!environment || environment.project.deletedAt !== null) return null; return toAuthenticated(environment); } ``` The resolved environment is then handed to TriggerTaskService.call (apps/webapp/app/v3/services/triggerTask.server.ts), which trusts the passed authenticatedEnvironment and performs no further authorization. The replay loader does constrain the environment picker to the run's own project (replay.ts around 178-184), but the action does not re-impose that constraint, so the server-side control is missing (UI-gated only). ### PoC As an authenticated user who owns a run in their own org (run param runParam), and who knows a target environment id belonging to another org or project (a CUID, obtained or guessed): ```http POST /resources/taskruns/<own runParam>/replay Content-Type: application/x-www-form-urlencoded environment=<environment id of the victim org/project>&failedRedirect=/... ``` The replay resolves the victim environment with no membership check and calls TriggerTaskService against it, creating a run in the victim tenant. Live-validated: a Postgres database was seeded with two tenants (orgA with env_A, orgB with env_B_victim) and the verbatim findEnvironmentById lookup (findFirst joining the environment to its project, `WHERE id = $1`, the exact query the replay path runs) was executed with the victim env id as an orgA attacker. Captured: ``` [002 replay findEnvironmentById] attacker(orgA) supplied env_B_victim -> {"id":"env_B_victim","organization_id":"orgB","project_id":"proj_B","deleted_at":null} CROSS-TENANT: CONFIRMED (resolved another org's env with no membership check -> replay would run there) ``` The lookup returned another organization's environment with no membership filter. That the replay action passes the client-supplied environmentId straight into this lookup (and TriggerTaskService trusts the returned env) is verified at source. ### Impact An authenticated user can create task runs in another tenant's environment (including production), consuming that tenant's queue concurrency and compute and inserting entries into their run history. Practical exploitation requires knowing a target environment id (not enumerable) and, for the injected run to actually execute rather than error, a task identifier that exists in the target environment; this bounds reliability but not the unauthorized cross-tenant write itself. ### Remediation In the replay action (or in ReplayTaskRunService), require the resolved target environment to belong to the source run's project/organization, for example assert `environment.projectId === existingTaskRun.projectId`, or re-validate the supplied environment id against the caller's membership (findEnvironmentBySlug with the validated projectId, or a members.some.userId filter), mirroring the constraint the loader already applies to the picker.

Last activity 2 Oct 2026. Patch Forecast updates every hour.