fix(security): pin outbound webhook and media fetches to the SSRF-validated IP#338
Closed
rmyndharis wants to merge 1 commit into
Closed
fix(security): pin outbound webhook and media fetches to the SSRF-validated IP#338rmyndharis wants to merge 1 commit into
rmyndharis wants to merge 1 commit into
Conversation
…idated IP The host safety check and the actual connection resolved DNS independently, so a hostile low-TTL DNS answer could pass validation as a public address and then have the connection re-resolve to an internal one (DNS rebinding). Connections now reuse the address vetted by the guard via an undici dispatcher pinned to it, preserving the original hostname for TLS SNI and the Host header and offering all vetted addresses so A-record failover still works. Routes webhook delivery (direct, queued, and test), server-side media downloads, and the whatsapp-web.js send-media-by-URL path (previously fetched via MessageMedia.fromUrl, which re-resolved DNS independently) through a single guarded helper. Adds undici as a dependency for the IP pinning.
Merged
rmyndharis
added a commit
that referenced
this pull request
Jun 19, 2026
* fix(webhook): deliver session lifecycle events and key webhook hardening (#335) * fix(security): pin outbound webhook and media fetches to validated IP (#338) * fix(plugins): persist plugin enable/config and restore (#339) * fix(message): persist bulk-sent messages, sanitize SSRF (#340) * fix(engine): return the real id for forwarded messages (#341) * fix(security): harden outbound requests, IPv6 SSRF (#344) * fix(security): secret-file perms, key pepper, allowedIps (#345) * fix(storage): bound tar imports, contain storage keys (#346) * fix(session): reconcile late acks, serialize reactions (#348) * fix(contract): webhook timeout, bounded shutdown, 501 (#350) * feat(session): force-kill a stuck session (#352) * merge #343 * merge #351 * chore(release): v0.4.3 — CHANGELOG, version bump (package.json/dashboard/swagger), README + docs
Owner
Author
|
Shipped in v0.4.3 (integrated via the release PR #354 and tagged |
This was referenced Jun 20, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The SSRF guard and the actual outbound connection resolved DNS independently:
assertSafeFetchUrl(url)did its owndns.lookupto validate the host, thenfetch(url)performed a second, unrelated resolution at connect time. An attacker controlling a domain's authoritative DNS with a ~0 TTL could answer a public address to the guard and an internal one (loopback, RFC1918,169.254.169.254cloud metadata) to the fetch — a classic DNS-rebinding TOCTOU. This affected webhook delivery (the OPERATOR-set URL) and server-side media fetches.Fix
A single guarded helper,
withSafeFetch, that resolves and validates the host once, then pins the connection to the vetted IP via an undici dispatcher whoseconnect.lookupreturns only those addresses — so the connection cannot be re-resolved between check and connect. Key invariants preserved:Hostheader stay the original hostname (not the IP), so certificate validation and virtual hosting are unaffected.redirect: 'manual'+assertNoRedirect) — the guard only validated the original host.WEBHOOK_SSRF_PROTECTopt-out andSSRF_ALLOWED_HOSTSescape-hatch behave exactly as before; literal-IP and allowlisted-host targets need no pin.Routed through it: webhook delivery (direct, queued, and the test endpoint), the engine-neutral media download, and the whatsapp-web.js send-media-by-URL path — which previously used
MessageMedia.fromUrl(bundlednode-fetch, its own unpinned re-resolution) and is the default engine, so it was still exploitable. It now fetches via the pinned helper and buildsMessageMediafrom the bytes, mirroring the Baileys path.Dependency
Adds
undici(the official Node HTTP client — the engine behind globalfetch), pinned to v6 to match Node 22's bundled major. Globalfetchexposes no per-request DNS hook; undici's dispatcher is the documented way to pin a connection's IP while keeping SNI. No audit advisories.Compatibility
No wire-format or response-shape change; non-breaking. SemVer: patch.
Tests
ssrf-guard.spec.ts:pinnedLookupreturns the captured addresses and never re-resolves (rebind-immune);withSafeFetchpasses a real pinned dispatcher for a hostname target (a regression that removes the pin fails this), refuses redirects on the pinned path, and is fail-closed before any socket.load-remote-media.spec.ts/whatsapp-web-js.adapter.spec.ts: media now flows through the pinned fetch and never viaMessageMedia.fromUrl.