---
id: GHSA-q567-cr4x-96w4
title: 'Trigger.dev: Blind SSRF via alert-channel webhook'
summary: 'Trigger.dev: Blind SSRF via alert-channel webhook'
severity: medium
cvss: 5.4
cwe:
  - CWE-918
vendor: trigger.dev
product: trigger.dev
ecosystem: npm
affected:
  - trigger.dev <= 4.5.1
patched:
  - trigger.dev 4.5.2
published: '2026-10-02'
updated: '2026-10-02'
sourceUpdated: '2026-10-02T22:35:32Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-q567-cr4x-96w4'
references:
  - url: >-
      https://github.com/triggerdotdev/trigger.dev/security/advisories/GHSA-q567-cr4x-96w4
  - url: 'https://github.com/triggerdotdev/trigger.dev/pull/4199'
  - url: >-
      https://github.com/triggerdotdev/trigger.dev/commit/34b1a181c2a1d33a53ebab88f84b05f81fea4254
  - url: 'https://github.com/triggerdotdev/trigger.dev/releases/tag/v4.5.2'
  - url: 'https://github.com/advisories/GHSA-q567-cr4x-96w4'
tags:
  - ghsa
  - npm
ingestedAt: '2026-10-02T23:34:57.407Z'
---

## Overview

### Summary

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.

### Affected
`apps/webapp`, HEAD `5d99457` (current main).

### Root cause
- Source (API route): `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).
- Store: `app/v3/services/alerts/createAlertChannel.server.ts` persists `{url, secret, version}` verbatim.
- Sink: `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.

### Runtime PoC (proven on self-host)
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.

### Severity
`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.

### Suggested fix
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.

### Dup
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.)

## Affected packages

- `trigger.dev <= 4.5.1`

## Remediation

Upgrade to a patched release:

- `trigger.dev 4.5.2`
