Trigger.dev: Cross-environment deployment cancelGHSA-4672-hwv6-gq62
MEDIUMFix: triggerdotdev/trigger.dev#4316GHSA-4672-hwv6-gq62 is a medium-severity (CVSS 5.4) CWE-639 vulnerability in trigger.dev. A fix is available for trigger.dev — see the affected versions and patch details below.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
GHSA-4672-hwv6-gq62 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.
trigger.devnpmDescription
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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | trigger.dev | all versions | 4.5.6npm install trigger.dev@4.5.6 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for trigger.dev, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update trigger.dev to 4.5.6 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-4672-hwv6-gq62 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
Frequently Asked Questions
Is GHSA-4672-hwv6-gq62 in your dependencies?
Find it across npm, including transitive dependencies.