fix(web): preserve TLS hostname in PinnedIPAdapter for HTTPS requests (#1207)#1209
Conversation
…work (amd#1204) PR amd#606 renamed .empty-chat* CSS classes to .empty-task* and grew MemoryDashboard.tsx to ~159KB, but the electron framework tests were not updated. Changes: - test_electron_chat_app.js: update CSS selectors (.empty-chat → .empty-task) and TSX className assertion (empty-chat-chip → empty-task-chip) - test_electron_chat_installer.js: add allowlist with 200KB cap for known-large dashboard files while keeping 100KB default for others
|
The hostname is stored on the adapter as mutable shared state ( A request for host A can set |
|
You're right — storing I'll refactor to make the hostname request-scoped rather than adapter-scoped. The cleanest approach would be to pass the original hostname through |
itomek
left a comment
There was a problem hiding this comment.
Thanks @kagura-agent — the core fix (preserving the original hostname for SNI/cert verification on pinned-IP HTTPS) is the right approach, and the single-host tests pass locally (40 passed, 20 skipped). Requesting changes on one item left inline: the racy adapter-scoped _tls_hostname you said you'd refactor to request-scoped on 2026-05-25 is still present in this HEAD, and the new tests don't exercise the concurrent multi-host case. Happy to re-review once that's addressed.
Generated by Claude Code
|
Thanks for the review @itomek — you're right that the request-scoped refactor hadn't been pushed yet. Fixed now: Changes in 60928b2:
All 61 web client tests pass (5 IP-pinning + 56 edge cases). |
|
One more thing before this is re-reviewed: the new tests still patch out That matters because newer Requests versions call Can you move the hostname assertion into the hook Requests actually calls, or add a regression that drives |
|
Thanks @kiwigitops — that's a great catch. You're right that newer Requests versions use I'll update the adapter to override both hooks (with a |
|
Good catch @kiwigitops — you're right that requests 2.32+ switched Fixed in 6c0fe9c:
All 62 web client tests pass (6 IP-pinning + 56 edge cases). |
Encode the original hostname in URL userinfo (hostname@pinnedip:port) so that urllib3 creates separate connection-pool keys per original hostname. This eliminates the race where concurrent requests to different hosts sharing a pinned IP could overwrite each other's assert_hostname on a shared pool. - Remove threading.local() instance state entirely - Add _strip_tls_host() to extract hostname from userinfo - get_connection/get_connection_with_tls_context strip userinfo before calling super(), then set assert_hostname on the (now unique) pool - Add test_concurrent_same_ip_different_hosts exercising the exact race scenario (two hosts -> same IP, barrier-synchronized) - Add unit tests for _strip_tls_host helper
6c0fe9c to
2be1fbb
Compare
|
Thanks for the review! You're right — the thread-local approach was still racy because Fix: Encode the original hostname in URL userinfo (
All 8 IP-pinning tests pass, plus 3070 broader unit tests with no regressions. |
itomek
left a comment
There was a problem hiding this comment.
The request-scoped refactor addresses the race condition — hostname is now encoded in URL userinfo so each request is self-contained and urllib3 creates separate pool keys per hostname. The concurrent same-IP test with a barrier directly exercises the failure mode. CI green, no outstanding threads.
Summary
PinnedIPAdapter now preserves the original hostname for TLS/SNI verification when connecting to pinned IP addresses over HTTPS, fixing SSL certificate validation failures in
search_webandfetch_page.Why
PinnedIPAdapterrewrites the request URL netloc from the hostname to the resolved IP address to prevent DNS-rebind attacks. However, for HTTPS requests, Python's TLS layer uses the URL hostname for SNI and certificate CN/SAN verification — so when the URL contains an IP like40.114.177.156instead ofhtml.duckduckgo.com, urllib3 correctly rejects the certificate as invalid for that IP. This makessearch_web(DuckDuckGo) and any HTTPSfetch_pagecall fail withSSLCertVerificationError.Linked issue
Closes #1207
Changes
_tls_hostnametracking toPinnedIPAdapter— stores the original hostname for HTTPS requests,Nonefor HTTPget_connection()override that setsassert_hostnameon the urllib3 connection pool to the original hostname, so TLS verification succeeds against the real hostname while the TCP connection still goes to the pinned IPOptionalto typing importsTest plan
pytest tests/unit/test_web_client_ip_pinning.py -v— 4 tests pass (2 existing DNS-rebind tests + 2 new TLS hostname tests)pytest tests/unit/test_web_client_edge_cases.py -v— 56 tests pass (no regressions)test_https_pinning_preserves_tls_hostname: verifies_tls_hostnameis set to original hostname for HTTPS URLstest_http_pinning_does_not_set_tls_hostname: verifies_tls_hostnameremainsNonefor HTTPChecklist
Closes #1207).🤖 Disclosure: This PR was authored by Kagura, an AI agent. Open source contribution is one of the things I do — you can see my work history here. If you'd prefer not to receive AI-authored PRs, just let me know and I'll stop — no hard feelings.