GHSA-p28v-f755-9qrg is a high-severity (CVSS 8.5) CWE-1321 vulnerability in @trigger.dev/core. O3 Security confirms whether GHSA-p28v-f755-9qrg is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Trigger.dev: Prototype pollution via run metadata operations → process-wide cross-tenant DoS
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.dev/corenpmDescription
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) — theJSONHeroPath(operation.key).set()sink is present from the first commit ofoperations.ts. SDK was at3.3.8at that time. - Still present at HEAD (SDK
4.5.0-rc.7);operations.tsunchanged since 2025-05, and@jsonhero/pathis pinned at^1.0.21(no proto-guard) throughout. - Affected range:
>= v3.3.8(metadata operations API) throughv4-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, …):
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/[email protected] 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 (tr_dev_…) and any run id of that environment.
curl -X PUT "http://localhost:8030/api/v1/runs/run_cmqr2bsyo00013js2twwhdsfu/metadata" \
-H "Authorization: Bearer tr_dev_<env_key>" -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:
- 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` - Cross-tenant authentication break — the next request from a different client (a worker's
POST /engine/v1/dev/dequeue) fails insidefindEnvironmentByApiKey:
i.e. one tenant's request breaks authentication for other tenants' workers → their jobs stop being dequeued/processed.prisma.runtimeEnvironment.findFirst({ where:{ apiKey:"…", polluted:"PWNED" }, include:{ project:true, …, polluted:"PWNED" } }) -> PrismaClientValidationError - Denial of service — full process crash. The
uncaughtExceptionin 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
uncaughtExceptioncrashes 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
- Reject dangerous path segments in
operation.keybefore building the path — block__proto__,constructor,prototype(and validate the$.-rooted JSONHero path shape). - Build metadata on a null-prototype object (
Object.create(null)) and/or use a pollution-safe setter, so__proto__cannot reachObject.prototype. - Upgrade/replace
@jsonhero/pathfor a version that is prototype-pollution safe, or wrap its.set()with a guard.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | @trigger.dev/core | ≥ 3.3.8&&< 4.5.6 | 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/core. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update @trigger.dev/core to 4.5.6 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-p28v-f755-9qrg 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.
How O3 protects you
O3 pinpoints whether GHSA-p28v-f755-9qrg is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-p28v-f755-9qrg. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-p28v-f755-9qrg in your dependencies?
O3 detects GHSA-p28v-f755-9qrg across npm dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.