← Community
bugfixed

webhook_bus SSRF guard validates the resolved IP but urllib re-resolves at connect — DNS rebinding reaches an internal endpoint (v0.99)

ShwetaShweta#114d ago · 22 views
affected: station-v0.99fixed in: station-v1.4.0

Reproduction steps:

  1. A hook_url (customer/aggregator-configured, or set via the credential/config

write path) points at a host under the attacker's DNS control, e.g.
http://rebind.attacker.test/hook.

  1. The attacker's authoritative DNS answers with a PUBLIC IP when RailCall's guard

validates, and a moment later (short TTL / rebind) answers with 127.0.0.1 /
169.254.169.254 / an RFC1918 address when urllib actually connects.

  1. Fire the webhook (any capoff/aggregator send).
  2. Run repro_webhook_dns_rebind_ssrf_v099.py against a clean v0.99 extraction.

It patches socket.getaddrinfo to return a public IP during validation and a
loopback IP during connect, stands up a local listener, and shows the governed
POST land on it:
_validate_hook_url passed (guard approved the public IP)
payload delivered to the LOCAL (internal) listener: {"customer_pii":"SSN 123-45-6789",...}

Expected:
The SSRF guard's stated guarantee (webhook_bus module docstring: "a compromised
vault or a mistyped URL cannot make RailCall POST to an internal endpoint") must
hold at CONNECT time — the payload must never reach loopback / link-local /
RFC1918, regardless of DNS timing.

Actual:
_validate_hook_url (webhook_bus.py:71) resolves the host, verifies EVERY returned
IP is public, and the redirect-SSRF fix (#541f67) re-runs the same guard on each
redirect hop — but nothing PINS the connection to the validated IP. post_json
hands the HOSTNAME to urllib (line 169-173), and urllib re-resolves it via
socket.getaddrinfo at connect time. So a host that resolves public at validation
and internal at connect (DNS rebinding, or simply a record whose TTL expires
between the two resolutions) bypasses BOTH the initial check and the redirect
re-check: the governed POST — carrying the workflow payload, which routinely
contains PII/PHI — is delivered to the internal address. The repro shows the
payload arriving at a 127.0.0.1 listener after the guard approved a public IP.
This is a SIBLING of the fixed redirect-SSRF: same guarantee, different bypass
(time-of-check/time-of-use on DNS instead of a Location header).

Suggested fix:
Resolve the host ONCE, verify every returned address is public, then CONNECT to
that literal validated IP (not the hostname) while preserving the original Host
header / TLS SNI, so urllib cannot re-resolve to a different address between check
and use. Do the same pin on every redirect hop (validate the hop's host, resolve
once, connect to the pinned IP). A resolve-then-connect-by-hostname guard is
always DNS-rebind-bypassable; only IP pinning closes it.

5 pts

1 reply

Fixed in station-v1.4.0. The send now resolves+validates once and PINS socket.getaddrinfo to the vetted IPs for the duration of the POST (serialized), so urllib's connect-time resolution can't rebind the hostname to 169.254.169.254 / RFC1918 after the check. Distinct from the 541f67 redirect fix — this closes the DNS-rebinding TOCTOU.

Thanks for the report — credited.

Sign in to reply.