Skip to content

Commit cbb3b23

Browse files
authored
Merge pull request #85 from abhimehro/copilot/sub-pr-72
Enhance SSRF DNS validation with comprehensive IP checks and IPv6 support
2 parents f96c46e + 035d3b9 commit cbb3b23

2 files changed

Lines changed: 21 additions & 13 deletions

File tree

.jules/sentinel.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@
4242

4343
## 2025-01-24 - [SSRF via DNS Resolution]
4444
**Vulnerability:** The `validate_folder_url` function blocked private IP literals but failed to resolve domain names to check their underlying IPs. An attacker could use a domain (e.g., `local.test`) that resolves to a private IP (e.g., `127.0.0.1`) to bypass the SSRF protection and access internal services.
45+
**Learning:** Blocking IP literals is insufficient for SSRF protection. DNS resolution must be performed to verify that a domain does not resolve to a restricted IP address. Additionally, comprehensive checks are needed for all dangerous IP ranges (private, loopback, link-local, reserved, multicast).
46+
**Prevention:**
47+
1. Resolve domain names using `socket.getaddrinfo` before making requests.
48+
2. Verify all resolved IP addresses against private/loopback/link-local/reserved/multicast ranges.
49+
3. Handle DNS resolution failures securely (fail-closed), catching both `socket.gaierror` and `OSError`.
50+
4. Strip IPv6 zone identifiers before IP validation.
51+
**Known Limitations:**
52+
- DNS rebinding attacks (TOCTOU): An attacker's DNS server could return a safe IP during validation and a private IP during the actual request. This is a fundamental limitation of DNS-based SSRF protection.
4553
**Learning:** Blocking IP literals is insufficient for SSRF protection. DNS resolution must be performed to verify that a domain does not resolve to a restricted IP address. Note that check-time validation has TOCTOU limitations vs request-time verification.
4654
**Prevention:**
4755
1. Resolve domain names using `socket.getaddrinfo` before making requests.

main.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -206,29 +206,29 @@ def validate_folder_url(url: str) -> bool:
206206

207207
try:
208208
ip = ipaddress.ip_address(hostname)
209-
if ip.is_private or ip.is_loopback:
210-
log.warning(f"Skipping unsafe URL (private IP): {sanitize_for_log(url)}")
209+
if (ip.is_private or ip.is_loopback or
210+
ip.is_link_local or ip.is_reserved or
211+
ip.is_multicast):
212+
log.warning(f"Skipping unsafe URL (restricted IP): {sanitize_for_log(url)}")
211213
return False
212214
except ValueError:
213215
# Not an IP literal, it's a domain. Resolve to check for private IPs.
216+
# Note: This check has a Time-Of-Check-Time-Of-Use (TOCTOU) limitation.
217+
# DNS rebinding attacks could return a safe IP during validation and a
218+
# private IP during the actual request. This is a known limitation of
219+
# DNS-based SSRF protection.
214220
try:
215221
# Resolve hostname to IPs
216222
# Note: This check has a Time-Of-Check-Time-Of-Use (TOCTOU) limitation.
217223
# A malicious DNS server could return a safe IP now and a private IP later.
218224
addr_infos = socket.getaddrinfo(hostname, None)
219225
for family, kind, proto, canonname, sockaddr in addr_infos:
220226
ip_str = sockaddr[0]
221-
222-
# Strip IPv6 zone identifier if present (e.g., fe80::1%eth0)
223-
if '%' in ip_str:
224-
ip_str = ip_str.split('%', 1)[0]
225-
226-
resolved_ip = ipaddress.ip_address(ip_str)
227-
228-
if (resolved_ip.is_private or
229-
resolved_ip.is_loopback or
230-
resolved_ip.is_link_local or
231-
resolved_ip.is_reserved or
227+
# Strip IPv6 zone identifier (e.g., "%eth0") if present
228+
ip_no_zone = ip_str.split('%', 1)[0]
229+
resolved_ip = ipaddress.ip_address(ip_no_zone)
230+
if (resolved_ip.is_private or resolved_ip.is_loopback or
231+
resolved_ip.is_link_local or resolved_ip.is_reserved or
232232
resolved_ip.is_multicast):
233233
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to restricted IP {resolved_ip}): {sanitize_for_log(url)}")
234234
return False

0 commit comments

Comments
 (0)