GitHub Advisoryadvisory · npm
GHSA-4672-hwv6-gq62 Trigger.dev: Cross-environment deployment cancel
MEDIUM
Official page ↗
- Ecosystem
- npm
- Packages
- trigger.dev
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L
- Weaknesses
- CWE-639
- Reviewed
- 2026-10-02
Affected packages
• npm trigger.dev: < 4.5.6 → fixed in 4.5.6
Description
Summary
Trigger.dev isolates each project into multiple environments (`dev`, `staging`, `prod`, and per-PR `preview` branches), each with its own secret API key — the environment is a trust boundary (a `dev`/preview/CI key is lower-trust than a `prod` key). Most API routes enforce this by scoping resource lookups to the authenticated key's environment (`where: { friendlyId, runtimeEnvironmentId: auth.environment.id }`).
The deployment cancel path does not. `DeploymentService.getDeployment()` scopes the lookup by `projectId` only — never `environmentId` — so a secret key for *any* environment in a project can cancel a deployment belonging to *any other* environment of the same project, including production. The deployment GET route, by contrast, *is* env-scoped — so the same key that is 404'd when trying to read a prod deployment can nonetheless cancel it. That asymmetry is the bug.
Affected
`apps/webapp`, HEAD `5d99457` (current `main`). Affects self-hosted and cloud.
Root cause
`apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts` authenticates to an environment and calls `deploymentService.cancelDeployment(authenticatedEnv, deploymentId, ...)`.
`apps/webapp/app/v3/services/deployment.server.ts`:
```ts
public cancelDeployment(authenticatedEnv: Pick, friendlyId, ...) {
return this.getDeployment(authenticatedEnv.projectId, friendlyId) // projectId only
.andThen(validateDeployment) // rejects only FINAL statuses
.andThen(cancelDeployment); // updateMany -> status CANCELED
}
private getDeployment(projectId: string, friendlyId: string) {
return this._prisma.workerDeployment.findFirst({
where: { friendlyId, projectId }, // ` — it discards the environment identity. `validateDeployment` blocks only `FINAL_DEPLOYMENT_STATUSES`, so any in-progress deployment (`PENDING`/`INSTALLING`/`BUILDING`/`DEPLOYING`) is cancellable.
Contrast — the correctly env-scoped sibling `api.v1.deployments.$deploymentId.ts` (GET):
```ts
const deployment = await prisma.workerDeployment.findFirst({
where: { friendlyId: deploymentId, environmentId: authenticatedEnv.id }, // env-scoped
});
```
Reads are env-scoped; the cancel mutation is not. The same `getDeployment(authenticatedEnv.projectId, …)` helper also backs the deployment *progress* methods (`deployment.server.ts:172,329`), so the projectId-only scope is a small class.
Runtime PoC (proven on the self-host stack)
Seeded one project with a `prod` env (key `tr_prod_…`) and a `dev` env (key `tr_dev_…`) and one `WorkerDeployment` (`deployment_pocvictim`, status `DEPLOYING`) in the prod env. As the dev key:
```
status BEFORE : DEPLOYING
GET /api/v1/deployments/deployment_pocvictim (dev key) -> 404 (env-scoped read DENIES it)
POST /api/v1/deployments/deployment_pocvictim/cancel (dev key) -> 204
status AFTER : CANCELED (prod deploy canceled by the dev key)
CONTROL: same cancel with a DIFFERENT project's key -> 404 (projectId scope blocks cross-project)
```
The dev key cannot *read* the prod deployment (404) yet *cancels* it (204 → CANCELED); a different project's key is correctly 404'd — so the gap is precisely cross-environment within a project.
Related
Weakness · CWE (1)
- CWE-639 Authorization Bypass Through User-Controlled Key weakness
References
- https://github.com/triggerdotdev/trigger.dev/security/advisories/GHSA-4672-hwv6-gq62 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