GHSA-p28v-f755-9qrg: High severity npm/@trigger.dev/core vulnerability
Summary
The run-metadata update endpoint PUT /api/v1/runs/:runId/metadata applies client-supplied "operations" by passing the attacker-controlled operation.key straight into new JSONHeroPath(operation.key).set(newMetadata, value) (packages/core/src/v3/runMetadata/operations.ts:22-23), with no prototype-pollution guard (@jsonhero/path@^1.0.21 does not reject proto/constructor/prototype).
A request with key: "$.proto.polluted" sets Object.prototype.polluted in the webapp process. Because every plain object then inherits that property, it corrupts unrelated code process-wide and across tenants — including Prisma query building and the Prometheus metrics client — causing query failures, broken authentication for other tenants' workers, and an uncaughtException (denial of service). Only a normal, low-privilege environment API key is required (one request).
Severity
A single request from any holder of a normal environment API key contaminates Object.prototype in the shared webapp process, breaking other tenants' workers (scope change) and degrading/ crashing the process (high availability impact). Prototype pollution is also a primitive for further gadget chains (integrity/confidentiality).
Affected versions
- Introduced in commit 34f8bd588 ("Add ability to update parent and root run metadata from children", PR #1563, 2025-01-08) — the JSONHeroPath(operation.key).set() sink is present from the first commit of operations.ts. SDK was at 3.3.8 at that time. - Still present at HEAD (SDK 4.5.0-rc.7); operations.ts unchanged since 2025-05, and @jsonhero/path is pinned at ^1.0.21 (no proto-guard) throughout. - Affected range: >= v3.3.8 (metadata operations API) through v4-beta / current v4.x — fixed: 4.5.6.
Root cause
packages/core/src/v3/runMetadata/operations.ts — applyMetadataOperations() builds a path from the untrusted operation.key and writes to it, for every operation type (set, append, increment, …):
ts const path = new JSONHeroPath(operation.key); // operation.key fully attacker-controlled path.set(newMetadata, operation.value); // no proto/constructor/prototype rejection
The request schema (UpdateMetadataRequestBody) types key as a plain string with no validation, and @jsonhero/path@1.0.21 walks proto as an ordinary segment → the assignment lands on Object.prototype.
Proof of Concept
Self-host ghcr.io/triggerdotdev/trigger.dev:v4-beta. Authenticated with a normal environment API key (trdev…) and any run id of that environment.
bash curl -X PUT "http://localhost:8030/api/v1/runs/runcmqr2bsyo00013js2twwhdsfu/metadata" \ -H "Authorization: Bearer trdev<envkey>" -H "Content-Type: application/json" \ --data '{"operations":[{"type":"set","key":"$.proto.polluted","value":"PWNED"}]}'
Result — Object.prototype.polluted = "PWNED" process-wide. Observed in the webapp logs:
1. The request's own query is corrupted — polluted:"PWNED" injected into every object Prisma enumerates: prisma.taskRun.updateMany({ where:{ id:"…", metadataVersion:2, polluted:"PWNED" }, data:{ …, metadataVersion:{ increment:1, polluted:"PWNED" }, polluted:"PWNED" }, polluted:"PWNED" }) -> Unknown argument polluted 2. Cross-tenant authentication break — the next request from a different client (a worker's POST /engine/v1/dev/dequeue) fails inside findEnvironmentByApiKey: prisma.runtimeEnvironment.findFirst({ where:{ apiKey:"…", polluted:"PWNED" }, include:{ project:true, …, polluted:"PWNED" } }) -> PrismaClientValidationError i.e. one tenant's request breaks authentication for other tenants' workers → their jobs stop being dequeued/processed. 3. Denial of service — full process crash. The uncaughtException in prom-client (Error: Added label "polluted" is not included in initial labelset: [ 'kind' ]) crashes the webapp process. Demonstrated with a second tenant: Tenant B (a different org/env, with its own API key) had a working request (POST /engine/v1/dev/dequeue → HTTP 400, auth OK) before the attack; immediately after Tenant A's single attack request, B's request returned HTTP 000 (no response) — the whole multi-tenant webapp was down. Logs show the crash at 20:53:41 and the process auto-restarting ~3s later (FairQueue/ScheduleEngine started). Repeating the attack in a loop yields a crash-loop = sustained DoS for all tenants.
(Note: the metadata endpoint itself swallows the Prisma failure with ignoreError:true and still returns HTTP 200 — the damage is the process-wide contamination observed in the logs, not the endpoint's status code.)
Impact
A low-privilege caller (one normal environment API key, one request) pollutes Object.prototype in the shared multi-tenant webapp process, causing:
- Full cross-tenant denial of service — the resulting uncaughtException crashes the webapp process, taking the service down for all tenants (demonstrated: a second tenant's request returned HTTP 000 immediately after the attack). Repeating the request produces a crash-loop / sustained DoS. Even without the crash, contaminated Prisma queries break other tenants' worker authentication, halting job processing. - A prototype-pollution primitive usable for further gadget chains (auth/logic bypass, etc.).
Suggested remediation
1. Reject dangerous path segments in operation.key before building the path — block proto, constructor, prototype (and validate the $.-rooted JSONHero path shape). 2. Build metadata on a null-prototype object (Object.create(null)) and/or use a pollution-safe setter, so proto cannot reach Object.prototype. 3. Upgrade/replace @jsonhero/path for a version that is prototype-pollution safe, or wrap its .set() with a guard.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/@trigger.dev/coreto a version that resolves this vulnerability.Fixed in 4.5.6 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Patch 34f8bd588 - Upgrade
Upgrade
@jsonhero/pathto a version that resolves this vulnerability.Fixed in 1.0.21 - Configuration
Upgrade/replace @jsonhero/path for a version that rejects dangerous JSONHero path segments so that operation.key cannot traverse __proto__/constructor/prototype (e.g., reject any operation.key whose path attempts to use '__proto__', 'constructor', 'prototype', or a '$.'-rooted path with those segments) before calling new JSONHeroPath(operation.key).set(...).
@jsonhero/path prototype-pollution guard / proto-segment validation = enabled - Configuration
Add server-side validation for UpdateMetadataRequestBody.key to reject paths that include dangerous JSONHero path segments such as '__proto__', 'constructor', or 'prototype' (the request schema types key as a plain string with no validation today).
run-metadata operations endpoint (PUT /api/v1/runs/:runId/metadata) UpdateMetadataRequestBody.key validation = reject - Compensating control
As a mitigation while patching, restrict who can call the run-metadata update endpoint (PUT /api/v1/runs/:runId/metadata) so only trusted principals can update run metadata, since a low-privilege environment API key can trigger process-wide cross-tenant prototype pollution.
Event History
Frequently Asked Questions
What is the severity of GHSA-p28v-f755-9qrg?
The severity of GHSA-p28v-f755-9qrg is classified as high with a score of 8.5.
How do I fix GHSA-p28v-f755-9qrg?
To fix GHSA-p28v-f755-9qrg, you should update to the latest version of the npm/@trigger.dev/core package.
What are the potential impacts of GHSA-p28v-f755-9qrg?
The potential impacts of GHSA-p28v-f755-9qrg include denial of service and information leakage due to improper input handling.
What type of attack vector does GHSA-p28v-f755-9qrg utilize?
GHSA-p28v-f755-9qrg utilizes an attack vector involving unvalidated user input within a run-metadata update endpoint.
Which endpoint is affected by GHSA-p28v-f755-9qrg?
The endpoint affected by GHSA-p28v-f755-9qrg is the run-metadata update endpoint, specifically 'PUT /api/v1/runs/:runId/metadata'.