PT-2026-111183

تاريخ النشر

2026-10-02

·

تاريخ التحديث

2026-10-02

الوصف من

CVSS 3.1
5.4
متوسطة
المقاييسAV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L

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.
قابلية الاستغلال
إصلاح
التأثيرات
المورّدون
المنتجات المتأثرة
المعرّفات ذات الصلة
GHSA-4672-HWV6-GQ62
وجدت خطأً في الوصف؟ لديك ما تضيفه؟ لا تتردد في مراسلتنا 👾