GHSA-vg6v-j97m-h5xq @novu/application-generic: `validateUrlSsrf` permits CGNAT (100.64.0.0/10) destinations — affects Workflow HTTP request step + Webhook filter condition
Hi Novu team,Reporting an SSRF blocklist gap in the shared `validateUrlSsrf` guard. A complete self-contained reproduction is inlined below — copy the four files into a directory and run `docker compose up`, plus a single-file probe that runs against Node directly. Locally validated against HEAD `291817c`.SummaryNovu's shared SSRF guard `validateUrlSsrf(url)` is used before server-side requests to user-configured URLs. The guard resolves hostnames and blocks a regex list of private/reserved IP ranges, but it does not block `100.64.0.0/10` shared address space. As a result, Novu features protected by this guard can still send server-side requests to destinations such as `100.100.100.200` (Alibaba Cloud metadata service) and any other service reachable in `100.64.0.0/10`.Affected codeGuard:`libs/application-generic/src/utils/ssrf-url-validation.ts``isPrivateIp(...)` regex list at lines 9-28DNS resolution and address validation at lines 55-72Product call-sites:Workflow HTTP request step: `apps/worker/src/app/workflow/usecases/send-message/execute-http-request-step.usecase.ts` — calls `validateUrlSsrf(url)` at line 149, then uses `HttpClientService` to send the request.Webhook filter condition: `libs/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts` — calls `validateUrlSsrf(child.webhookUrl)` at line 265, then sends `axios.post(child.webhookUrl, ...)` at line 277.HTTP client:`libs/application-generic/src/services/http-client/http-client.service.ts` — uses `got(gotOptions)` at lines 120 and 142 after the preflight validation.Root causeThe SSRF guard uses a hand-written regex deny-list:This list omits `100.64.0.0/10`, also called shared address space or CGNAT. These addresses are not RFC1918 private addresses, but they are also not normal public-internet destinations. Cloud and infrastructure providers commonly use special-use address ranges for metadata and internal services; **Alibaba Cloud metadata is available at `100.100.100.200`**.Reproduction — Part 1: unit-level probe (no Docker required)Save the following file and run with `node novu_ssrf_guard_probe.js`. The script replicates `validateUrlSsrf` from `libs/application-generic/src/utils/ssrf-url-validation.ts` **verbatim** (the `isPrivateIp` regex list is copied as-is) and tests several URL categories.`novu_ssrf_guard_probe.js`Expected output (relevant lines)The 2nd and 3rd `ALLOW` rows are the bypass — both are non-public destinations the guard should refuse.Reproduction — Part 2: end-to-end Docker CGNAT proofSave the three files below into a directory, then:This mirrors the product sequence in `execute-http-request-step.usecase.ts`: resolve hostname → validate with `validateUrlSsrf` → send HTTP request. The "target" container is bound to a CGNAT address (`100.64.0.20`) on a custom subnet, simulando a cloud-internal service reachable on the CGNAT range.`docker-compose.yml``target.py``client.js`Expected outputThe chain is:Resolve hostname `cgnat-target` → `100.64.0.20` (a CGNAT address).Run Novu's `validateUrlSsrf` against the URL — returns `ALLOW` because `100.64.0.0/10` is missing from `isPrivateIp`.Send the actual server-side HTTP POST → reaches the CGNAT-bound target → response with marker `NOVU_CGNAT_SSRF_OK` is received.ImpactAny Novu feature that allows a user to configure an outbound HTTP URL and relies on `validateUrlSsrf` may still reach `100.64.0.0/10`. Impact is highest for:**Alibaba Cloud deployments**, where `http://100.100.100.200/latest/meta-data/` may expose instance metadata.**Self-hosted deployments** where `100.64.0.0/10` routes to private infrastructure, service meshes, VPNs, carrier-grade NAT, or provider-side internal services.**Multi-tenant deployments** where one tenant can configure workflow HTTP request steps or webhook filters that execute from shared worker/API infrastructure — cross-tenant SSRF primitive into provider-internal services.Suggested remediationReplace regex matching with IP parsing and CIDR classification, e.g. using `ipaddr.js` with `process(...)` to normalize IPv4-mapped IPv6.Treat only globally reachable public IPs as allowed by default (`addr.range() === 'unicast'` after IPv4-mapped unwrap, or equivalent).Explicitly deny all special-use ranges, including at least:`0.0.0.0/8`, `10.0.0.0/8`, `100.64.0.0/10`, `127.0.0.0/8`, `169.254.0.0/16`, `172.16.0.0/12`, `192.168.0.0/16`multicast (`224.0.0.0/4`), documentation (`192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`, `2001:db8::/32`), benchmarking (`198.18.0.0/15`), reserved (`240.0.0.0/4`)IPv6 ULA (`fc00::/7`), link-local (`fe80::/10`), loopback (`::1`), and the IPv4-mapped variants of all of the aboveAdd regression tests for:`100.64.0.1`, `100.100.100.200`hostnames resolving to those addressesIPv4-mapped variants of denied IPv4 ranges (e.g., `::ffff:100.64.0.1`)Consider connection-time validation or a guarded lookup agent so the actual request cannot resolve to a different IP than the preflight checked (DNS-rebinding TOCTOU mitigation).NotesThis report is intentionally scoped to the concrete `100.64.0.0/10` bypass. Additional missed ranges exist in the current regex guard (multicast `224.0.0.0/4`, broadcast `255.255.255.255`, benchmarking, documentation, `0.0.0.0/8` outside `/32`, and IPv4-mapped variants), but CGNAT is the highest-confidence real-world issue because it includes a known cloud metadata endpoint (`100.100.100.200` on Alibaba Cloud).
CVSS v3.1 score: 6.8