fix(http): remove dead auto-redirect SSRF footgun; prove the rebind window is closed at connect (LC-152-TOCTOU) #290
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/lc-152-pin-validated-ip"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #286 (LC-152-TOCTOU). Filed investigate-first; the investigation changed the conclusion, so the deliverable differs from the literal title - details below.
What the investigation found
The ticket asked to "pin outbound connections to the SSRF-validated IP / close the DNS-rebind re-resolve window." Reading
http_client.rsend to end, the live paths already do this:PublicOnlyResolveris the authoritative resolution reqwest connects on, and it validates public-only on exactly the addresses it returns.lc152_resolver_property_pair::reqwest_connects_to_resolver_supplied_ipproves reqwest uses the resolver's IPs (no silent re-resolve). So a DNS rebind to a private address between submit-time validation and connect-time resolution is refused at connect, not left open.validate_url), before reqwest's literal-IP fast path (which skips the resolver) can fire.outbound_getloop.So there is no exploitable rebind residual on any path in use. The prior "residual TOCTOU / known gap" prose overstated it.
The one real latent hole (now removed)
outbound_get_following_redirects+ itsPolicy::limitedclient relied on reqwest's auto-redirect, whose connector takes a literal-IP fast path that skipsdns::Resolve. A response that 302s to a literal private IP (http://169.254.169.254/...) would have been followed with neither guard layer firing - an SSRF bypass. It had zero callers, so it was dead code that was also unsafe-if-used. Removed it, the redirect client,MAX_REDIRECTS, and the redirect plumbing; the shared client is now unconditionallyPolicy::none. The module now documents that any future redirect-follower must use the per-hop-validated pattern (mirroring the desktop net_guard manual loop, LC-210).Hardening + proof
resolve_public_only(host)and added in-module tests that the connect-time layer (not justvalidate_url) refuses loopback / RFC1918 / metadata literals and a loopback hostname, and accepts a public literal - pinning the "the resolver is the real guard" guarantee that was only covered indirectly.ssrf.rs,bridge_avatar.rs, anddocs/protocol-bridges.md. The only genuinely deferred item is a single connect-layer IP-pin (defense-in-depth against a hypotheticalis_globally_routablebug), which still awaits a clean reqwest connector hook (Option A in the module doc).Validation
just test+just test-saas+just checkall green. New: 4 in-module resolver tests; existing 8 resolver-property + 4 error-propagation + grep-ban tests still pass.No
[operator-action]: the removed helper was never called, so no shipped path was exploitable - this is footgun removal + documentation accuracy, no behavior change for any in-use path.🤖 Generated with Claude Code