GHSA-q567-cr4x-96w4Medium· 5.4▾ SunlitTrigger.dev: Blind SSRF via alert-channel webhook
▾ Sunlit zone — Low / medium · no exploitation signal
impact 29.7 · likelihood 0 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
A WEBHOOK alert channel stores a user-supplied url. When an alert fires (deployment/run failure, error groups), the webapp server (alertsWorker -> DeliverAlertService) POSTs the HMAC-signed alert payload to that URL via fetch(webhook.url, ...). The URL is never validated against a host allowlist or private-IP/metadata blocklist (a repo-wide search for 169.254, isPrivate, isLoopback, net.isIP, ssrf returns ZERO hits), and the API route's URL field is just z.string().optional() (no syntax check at all). So an authenticated tenant can point the webhook at internal infrastructure or 169.254.169.254 and the multi-tenant server fetches it.
apps/webapp, HEAD 5d99457 (current main).
app/presenters/v3/ApiAlertChannelPresenter.server.ts ApiAlertChannelData.url = z.string().optional() (no host validation); route app/routes/api.v1.projects.$projectRef.alertChannels.ts (PAT-auth).app/v3/services/alerts/createAlertChannel.server.ts persists {url, secret, version} verbatim.app/v3/services/alerts/deliverAlert.server.ts:973 fetch(webhook.url, {method:"POST", headers:{"x-trigger-signature-hmacsha256":...}, body:rawPayload}); identical at deliverErrorGroupAlert.server.ts:258. Runs inside the webapp process; fetch follows redirects (IMDSv1-via-redirect). No egress guard anywhere.Seeded a project + STAGING env + a WEBHOOK channel with url=http://host.docker.internal:7766/... (an internal address from the server's POV) + a DEPLOYING deployment. Triggered via the public API POST /api/v1/deployments/deployment_ssrfpoc/fail -> 200 -> FAILED -> alertsWorker -> the server fetched the listener. Captured:
POST /ssrf-via-webhook HTTP/1.1
host: host.docker.internal:7766
x-trigger-signature-hmacsha256: <redacted>
user-agent: node
{"type":"alert.deployment.failed", ...}
The webapp server (user-agent: node) made an outbound POST to the attacker-chosen internal URL; 169.254.169.254 / any internal host works identically.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N ~ Medium. Blind (response not reflected) + POST-only (fixed body = signed alert payload), so it enables internal port/host scanning (delivery success/timing oracle), state-changing POSTs to internal services, and IMDSv1-via-redirect — not arbitrary GET exfiltration. PR:L (authenticated tenant). Egress private-IP/metadata blocking on server-issued webhooks is industry standard; its absence is the defect.
Validate the webhook URL on create (require http(s), resolve host, reject private/link-local/loopback/metadata ranges) AND re-check at fetch time (re-resolve after redirects or disable redirects / pin to resolved public IP). A shared assertPublicUrl(url) used by createAlertChannel + the two delivery sinks.
Distinct CWE-918 class from the IDOR advisories. FRESH. (The Grav maintainer independently hardened the same webhook-URL-SSRF class in grav-plugin-api commit dfcc947 on 2026-06-26 — corroborating the class is real and fixable.)
trigger.dev <= 4.5.1Upgrade to a patched release:
trigger.dev 4.5.2Connected by shared product, vendor, weakness, or advisory.
GHSA-pp95-gc86-jq6qHigh· 7.1Trigger.dev: Missing Authentication in Run Replay Action Allows Cross-Organization Task Execution (IDOR)
GHSA-qxpp-qjg8-x4jvHigh· 7.1Trigger.dev: Run replay injects a task run into an attacker-chosen environment (cross-tenant write)
GHSA-xxv7-2vv3-h682High· 7.7Trigger.dev: Server-side request forgery via unvalidated webhook alert-channel URL
CVE-2026-85651High· 8.5Trigger.dev versions before 4.5.2 fail to validate environment membership during run replay operations, allowing authenticated attackers to inject task runs into arbitrary environments
GHSA-gg6r-gp4c-89hpCriticalTrigger.dev: V1 coordinator default-secret unauth Socket.IO
GHSA-59h8-w5q6-mfmpMedium· 5.3Trigger.dev: Unauthenticated Realtime Stream Data Injection via Run FriendlyId