fix(notifications): close DNS-rebinding SSRF in webhook delivery#447
Conversation
The previous implementation called socket.getaddrinfo() in validate_config() to block internal addresses, then passed the original URL to urllib/urlopen which performed a second, independent DNS lookup. An attacker who controls a domain with a short TTL can resolve it to a public IP at validation time and then rebind the DNS to an internal address (e.g. 169.254.169.254) before the actual HTTP request fires — a classic TOCTOU SSRF. Fix: resolve the hostname exactly once in validate_config(), check the result against _BLOCKED_NETWORKS, and return the validated IP string to the caller. _post_once now receives that pre-validated IP and connects to it directly (using http.client.HTTPConnection/HTTPSConnection) so no second DNS query can be made. For HTTPS, a raw TCP socket is opened to the IP and then wrapped with ssl.create_default_context().wrap_socket(server_hostname=hostname) so SNI and certificate validation still use the original domain name. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughRefactored the notification delivery system to resolve hostnames upfront and bypass Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Solid fix for a real TOCTOU SSRF vulnerability. Implementation is clean, uses stdlib correctly, and eliminates the DNS rebinding attack vector. |
Summary
Security: The
NotificationServicehad a TOCTOU SSRF vulnerability in its webhook delivery path.validate_config()resolved the hostname and checked it against blocked ranges, but then passed the original URL tourllib.request.urlopen()which performed a second, independent DNS lookup. An attacker controlling a domain with TTL=0 could resolve it to a public IP at validation time, then rebind the DNS to an internal address (e.g.169.254.169.254AWS metadata,10.x.x.x, loopback) before the actual HTTP POST fires.Fix: The hostname is now resolved once in
validate_config(), which returns the validated IP string._post_once()receives that pre-validated IP and connects to it directly viahttp.client.HTTPConnection/HTTPSConnection, eliminating any second DNS query. For HTTPS, a raw TCP socket is opened to the IP and then TLS-wrapped withserver_hostname=<original_hostname>so SNI and certificate validation still use the domain name.Attack scenario (before fix)
evil.example.comwith TTL=1, initially pointing to203.0.113.1(public IP).https://evil.example.com/webhookas a push notification URL.validate_config()resolvesevil.example.com→203.0.113.1✅ passes block-list check.evil.example.com→169.254.169.254(AWS IMDSv1).urllib.request.urlopen()resolves again → hits instance metadata service 🔴.Changes
validate_config()now returns the resolved IP string (wasNone)._resolve_and_check_ip()centralises DNS resolution + block-list check._post_once()accepts aresolved_ipparameter and connects to it directly.urllibimport (no longer needed); addedhttp.clientandssl.validate_configcontinue to pass (mock patchessocket.getaddrinfo);_post_oncetests usepatch.objectso the signature change is transparent.Test plan
tests/unit/utils/test_notifications.py— all existing tests pass without modificationvalidate_config({"url": "http://localhost/webhook"})raisesValueError(loopback blocked)validate_config({"url": "https://example.com/webhook"})returns a public IP string_post_onceuses the returned IP and not the original hostname for TCP connection🤖 Generated with Claude Code
Summary by CodeRabbit