security: pin fetched connections to the vetted address (close DNS rebinding) - #415
Open
coderdailyone wants to merge 1 commit into
Open
security: pin fetched connections to the vetted address (close DNS rebinding)#415coderdailyone wants to merge 1 commit into
coderdailyone wants to merge 1 commit into
Conversation
…binding) guard.get_checked previously resolved a name for the address check and then let the client resolve it again to connect, so a ~0-TTL record could flip to 127.0.0.1 (or the metadata endpoint) between the two — the exact gap the module docstring called out as "not covered". Every hop now connects to the address that passed its check: the request URL carries the vetted IP, Host keeps the original name (and explicit port) for virtual hosting, and the sni_hostname extension keeps the TLS handshake — and certificate verification — against the name. Literal-IP URLs are unchanged. Redirect Locations are resolved against the logical URL, and the final logical URL rides resp.extensions["logical_url"] so web_fetch reports the name, not the pinned address. Verified against a local TLS server whose cert carries only DNS:pinned.test: the pinned request (URL=127.0.0.1, sni_hostname=pinned.test) verifies and the server sees Host: pinned.test; the same request without the extension fails with "IP address mismatch" — httpcore does verify certificates against sni_hostname, not the URL host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
coworker/web/guard.py's own docstring stated the gap: "Not covered: DNS rebinding. The name is resolved here and resolved again by the client when it connects… Closing that needs connection-level IP pinning; the hop check is the cheap 90%." This PR is that pinning.What was broken:
get_checkedvetted the resolved addresses, then handed the name back to httpx, which resolved it a second time. A ~0-TTL record can answer with a public IP for the check and 127.0.0.1 (or 169.254.169.254) for the connect — andweb_fetchisrequires_approval=False, so nothing ever prompts.Fix: every hop now connects to the exact address that passed its check — the request URL carries the vetted IP,
Hostkeeps the original name (and explicit port) for virtual hosting, and thesni_hostnamerequest extension keeps the TLS handshake and certificate verification against the name. Literal-IP URLs are unchanged. RedirectLocations resolve against the logical URL (a relative Location must stay on the original host), and the final logical URL ridesresp.extensions["logical_url"]soweb_fetchreports the name, not the pinned address.browser_read_urlinherits the fix throughget_checked.Verification (terminal evidence in place of screenshots — no UI in this change):
tests/test_url_address_guard.py: 34/34, six new tests — pin target + Host + SNI, a rebinding name that flips to loopback after the check, explicit-port Host, IPv6 bracketing, literal passthrough, logical-URL reporting:DNS:pinned.test(no IP SAN): the pinned request (URL=https://127.0.0.1:<port>/,sni_hostname=pinned.test) verifies and returns 200 with the server seeingHost: pinned.test; the identical request without the extension fails withcertificate verify failed: IP address mismatch— confirming httpcore verifies certificates againstsni_hostname, not the URL host.Residual, stated in the updated docstring:
browser_open_url's pre-check still resolves twice — the browser owns its own connections and cannot be pinned from here.🤖 Generated with Claude Code