---
id: GHSA-4672-hwv6-gq62
title: 'Trigger.dev: Cross-environment deployment cancel'
summary: 'Trigger.dev: Cross-environment deployment cancel'
severity: medium
cvss: 5.4
cwe:
  - CWE-639
vendor: trigger.dev
product: trigger.dev
ecosystem: npm
affected:
  - trigger.dev <= 4.5.5
patched:
  - trigger.dev 4.5.6
published: '2026-10-02'
updated: '2026-10-02'
sourceUpdated: '2026-10-02T22:42:43Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-4672-hwv6-gq62'
references:
  - url: >-
      https://github.com/triggerdotdev/trigger.dev/security/advisories/GHSA-4672-hwv6-gq62
  - url: 'https://github.com/triggerdotdev/trigger.dev/pull/4316'
  - url: >-
      https://github.com/triggerdotdev/trigger.dev/commit/6997aeb05e27d2db47f9eda01fdc8a17c81a1ae0
  - url: 'https://github.com/triggerdotdev/trigger.dev/releases/tag/v4.5.6'
  - url: 'https://github.com/advisories/GHSA-4672-hwv6-gq62'
tags:
  - ghsa
  - npm
ingestedAt: '2026-10-02T23:34:57.402Z'
---

## Overview

### 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<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):
```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.

## Affected packages

- `trigger.dev <= 4.5.5`

## Remediation

Upgrade to a patched release:

- `trigger.dev 4.5.6`
