{"schema":"threatline.record/2","record":{"id":"GHSA-4672-hwv6-gq62","aliases":["CVE-2026-114079","GHSA-4672-hwv6-gq62"],"summary":"Trigger.dev: Cross-environment deployment cancel","details":"### Summary\n\nTrigger.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 }`).\n\nThe 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.\n\n### Affected\n\n`apps/webapp`, HEAD `5d99457` (current `main`). Affects self-hosted and cloud.\n\n### Root cause\n\n`apps/webapp/app/routes/api.v1.deployments.$deploymentId.cancel.ts` authenticates to an environment and calls `deploymentService.cancelDeployment(authenticatedEnv, deploymentId, ...)`.\n\n`apps/webapp/app/v3/services/deployment.server.ts`:\n```ts\npublic cancelDeployment(authenticatedEnv: Pick<AuthenticatedEnvironment,\"projectId\">, friendlyId, ...) {\n  return this.getDeployment(authenticatedEnv.projectId, friendlyId)   // projectId only\n    .andThen(validateDeployment)                                      // rejects only FINAL statuses\n    .andThen(cancelDeployment);                                       // updateMany -> status CANCELED\n}\nprivate getDeployment(projectId: string, friendlyId: string) {\n  return this._prisma.workerDeployment.findFirst({\n    where: { friendlyId, projectId },        // <-- NO environmentId filter\n  });\n}\n```\n\n`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.\n\n**Contrast — the correctly env-scoped sibling** `api.v1.deployments.$deploymentId.ts` (GET):\n```ts\nconst deployment = await prisma.workerDeployment.findFirst({\n  where: { friendlyId: deploymentId, environmentId: authenticatedEnv.id },   // env-scoped\n});\n```\nReads 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.\n\n### Runtime PoC (proven on the self-host stack)\n\nSeeded 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:\n\n```\nstatus BEFORE                                                  : DEPLOYING\nGET  /api/v1/deployments/deployment_pocvictim   (dev key)      -> 404   (env-scoped read DENIES it)\nPOST /api/v1/deployments/deployment_pocvictim/cancel (dev key) -> 204\nstatus AFTER                                                   : CANCELED   (prod deploy canceled by the dev key)\nCONTROL: same cancel with a DIFFERENT project's key            -> 404   (projectId scope blocks cross-project)\n```\n\nThe 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.","categories":["Frameworks"],"product":"trigger.dev","vendor":"","ecosystem":"npm","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-4672-hwv6-gq62","source_modified_at":"2026-10-02T22:42:43Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"trigger.dev"},"range":"<= 4.5.5"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-4672-hwv6-gq62","source_modified_at":"2026-10-21T10:57:24.966429549Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"name":"trigger.dev","ecosystem":"npm","purl":"pkg:npm/trigger.dev"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"4.5.6"}]}],"versions":[]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-4672-hwv6-gq62","source_modified_at":"2026-10-02T22:42:43Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"trigger.dev"},"version":"4.5.6"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-4672-hwv6-gq62","source_modified_at":"2026-10-21T10:57:24.966429549Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"name":"trigger.dev","ecosystem":"npm","purl":"pkg:npm/trigger.dev"},"version":"4.5.6"}],"cvss":5.4,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-02T22:42:40Z","modified_at":"2026-10-21T10:57:24.966429549Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-4672-hwv6-gq62","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-02T22:42:43Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-4672-hwv6-gq62","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-21T10:57:24.966429549Z"}],"sources":["GitHub Advisory Database","OSV"],"references":[{"url":"https://github.com/triggerdotdev/trigger.dev/security/advisories/GHSA-4672-hwv6-gq62","source":"GitHub Advisory Database"},{"url":"https://github.com/triggerdotdev/trigger.dev/pull/4316","source":"GitHub Advisory Database"},{"url":"https://github.com/triggerdotdev/trigger.dev/commit/6997aeb05e27d2db47f9eda01fdc8a17c81a1ae0","source":"GitHub Advisory Database"},{"url":"https://github.com/triggerdotdev/trigger.dev/releases/tag/v4.5.6","source":"GitHub Advisory Database"},{"url":"https://github.com/advisories/GHSA-4672-hwv6-gq62","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"product","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-21T10:57:24.966429549Z","content_hash":"64819c01fc7be53a448aa126d0e72291f4db7349c2133fc8a9f978c7a8ed062a"}}
