Trigger.dev: Cross-environment deployment cancel
What fixes it
- 4.5.6trigger.dev, npmGitHub
npm install trigger.dev@4.5.6
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
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:
public cancelDeployment(authenticatedEnv: Pick<AuthenticatedEnvironment,"projectId">, 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 }, // <-- NO environmentId filter
});
}cancelDeployment accepts authenticatedEnv but its type is literally Pick<AuthenticatedEnvironment,"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):
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.
Text from GitHub Advisory Database
Affected versions 2
- trigger.dev, npm<= 4.5.5GitHub
- trigger.dev, npmfrom first release, fixed in 4.5.6OSV
References 5
- github.com/triggerdotdev/trigger.dev/commit/6997aeb05e27d2db47f9eda01fdc8a17c81a1ae0Fixvia GitHub
- github.com/triggerdotdev/trigger.dev/releases/tag/v4.5.6Fixvia GitHub
- github.com/triggerdotdev/trigger.dev/security/advisories/GHSA-4672-hwv6-gq62Advisoryvia GitHub
- github.com/advisories/GHSA-4672-hwv6-gq62Advisoryvia GitHub
- github.com/triggerdotdev/trigger.dev/pull/4316Referencevia GitHub