🛡️ Sentinel: [HIGH] Fix SSRF bypass via DNS resolution#101
🛡️ Sentinel: [HIGH] Fix SSRF bypass via DNS resolution#101google-labs-jules[bot] wants to merge 1 commit intomainfrom
Conversation
🚨 Severity: HIGH 💡 Vulnerability: The `validate_folder_url` function checked for private IP literals but did not resolve domains. This allowed an attacker to bypass the check using a domain that resolves to a private IP (e.g., `localtest.me` -> `127.0.0.1`), enabling Server-Side Request Forgery (SSRF) against internal services. 🎯 Impact: An attacker could force the application to make HTTP requests to internal services running on localhost or private networks, potentially exposing sensitive data or interacting with internal APIs. 🔧 Fix: Updated `validate_folder_url` to resolve hostnames using `socket.getaddrinfo` and verify that none of the resolved IPs are private or loopback addresses. ✅ Verification: Verified using a reproduction script that `localtest.me` is now blocked, while legitimate URLs and IP checks still function correctly.
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
Merging to
|
| # socket.getaddrinfo returns a list of tuples. sockaddr is the 5th element. | ||
| # For AF_INET/AF_INET6, sockaddr[0] is the IP address string. | ||
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type_, proto, canonname, sockaddr in addr_infos: |
Check warning
Code scanning / Prospector (reported by Codacy)
Unused variable 'family' (unused-variable) Warning
| ip_str = sockaddr[0] | ||
| ip = ipaddress.ip_address(ip_str) | ||
| if ip.is_private or ip.is_loopback: | ||
| log.warning(f"Skipping unsafe URL (domain resolves to private IP {ip_str}): {sanitize_for_log(url)}") |
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
| # If it's a real DNS error, httpx will fail later. | ||
| pass | ||
| except Exception as e: | ||
| log.warning(f"DNS resolution check failed for {sanitize_for_log(url)}: {e}") |
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
| 1. Parse URLs and check hostnames against `localhost` and private IP ranges using `ipaddress` module. | ||
| 2. Enforce strict length limits on user inputs (e.g., profile IDs) to prevent resource exhaustion or buffer abuse. | ||
|
|
||
| ## 2025-01-27 - [SSRF Protection via DNS Resolution] |
Check notice
Code scanning / Remark-lint (reported by Codacy)
Warn when references to undefined definitions are found. Note
| 1. Parse URLs and check hostnames against `localhost` and private IP ranges using `ipaddress` module. | ||
| 2. Enforce strict length limits on user inputs (e.g., profile IDs) to prevent resource exhaustion or buffer abuse. | ||
|
|
||
| ## 2025-01-27 - [SSRF Protection via DNS Resolution] |
Check notice
Code scanning / Remark-lint (reported by Codacy)
Warn when shortcut reference links are used. Note
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type_, proto, canonname, sockaddr in addr_infos: | ||
| ip_str = sockaddr[0] | ||
| ip = ipaddress.ip_address(ip_str) |
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "ip" doesn't conform to snake_case naming style Warning
| ip_str = sockaddr[0] | ||
| ip = ipaddress.ip_address(ip_str) | ||
| if ip.is_private or ip.is_loopback: | ||
| log.warning(f"Skipping unsafe URL (domain resolves to private IP {ip_str}): {sanitize_for_log(url)}") |
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (125/100) Warning
| # socket.getaddrinfo returns a list of tuples. sockaddr is the 5th element. | ||
| # For AF_INET/AF_INET6, sockaddr[0] is the IP address string. | ||
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type_, proto, canonname, sockaddr in addr_infos: |
Check notice
Code scanning / Pylint (reported by Codacy)
Unused variable 'canonname' Note
| # socket.getaddrinfo returns a list of tuples. sockaddr is the 5th element. | ||
| # For AF_INET/AF_INET6, sockaddr[0] is the IP address string. | ||
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type_, proto, canonname, sockaddr in addr_infos: |
Check notice
Code scanning / Pylint (reported by Codacy)
Unused variable 'family' Note
| # socket.getaddrinfo returns a list of tuples. sockaddr is the 5th element. | ||
| # For AF_INET/AF_INET6, sockaddr[0] is the IP address string. | ||
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type_, proto, canonname, sockaddr in addr_infos: |
Check notice
Code scanning / Pylint (reported by Codacy)
Unused variable 'type_' Note
| # socket.getaddrinfo returns a list of tuples. sockaddr is the 5th element. | ||
| # For AF_INET/AF_INET6, sockaddr[0] is the IP address string. | ||
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type_, proto, canonname, sockaddr in addr_infos: |
Check notice
Code scanning / Pylint (reported by Codacy)
Unused variable 'proto' Note
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type_, proto, canonname, sockaddr in addr_infos: | ||
| ip_str = sockaddr[0] | ||
| ip = ipaddress.ip_address(ip_str) |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "ip" doesn't conform to snake_case naming style Warning
| ip_str = sockaddr[0] | ||
| ip = ipaddress.ip_address(ip_str) | ||
| if ip.is_private or ip.is_loopback: | ||
| log.warning(f"Skipping unsafe URL (domain resolves to private IP {ip_str}): {sanitize_for_log(url)}") |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Line too long (125/100) Warning
| # If DNS resolution fails, or IP parsing fails, we pass. | ||
| # If it's a real DNS error, httpx will fail later. | ||
| pass | ||
| except Exception as e: |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "e" doesn't conform to snake_case naming style Warning
| # socket.getaddrinfo returns a list of tuples. sockaddr is the 5th element. | ||
| # For AF_INET/AF_INET6, sockaddr[0] is the IP address string. | ||
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type_, proto, canonname, sockaddr in addr_infos: |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused variable 'proto' Note
| # socket.getaddrinfo returns a list of tuples. sockaddr is the 5th element. | ||
| # For AF_INET/AF_INET6, sockaddr[0] is the IP address string. | ||
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type_, proto, canonname, sockaddr in addr_infos: |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused variable 'canonname' Note
| # socket.getaddrinfo returns a list of tuples. sockaddr is the 5th element. | ||
| # For AF_INET/AF_INET6, sockaddr[0] is the IP address string. | ||
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type_, proto, canonname, sockaddr in addr_infos: |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused variable 'type_' Note
| # socket.getaddrinfo returns a list of tuples. sockaddr is the 5th element. | ||
| # For AF_INET/AF_INET6, sockaddr[0] is the IP address string. | ||
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type_, proto, canonname, sockaddr in addr_infos: |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused variable 'family' Note
| ip_str = sockaddr[0] | ||
| ip = ipaddress.ip_address(ip_str) | ||
| if ip.is_private or ip.is_loopback: | ||
| log.warning(f"Skipping unsafe URL (domain resolves to private IP {ip_str}): {sanitize_for_log(url)}") |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Use lazy % formatting in logging functions Note
| # If it's a real DNS error, httpx will fail later. | ||
| pass | ||
| except Exception as e: | ||
| log.warning(f"DNS resolution check failed for {sanitize_for_log(url)}: {e}") |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Use lazy % formatting in logging functions Note
|
Superseded: SSRF DNS resolution fix already in main via PR #109. |
Understood. Acknowledging that this work is now obsolete and stopping work on this task. |
Added DNS resolution to
validate_folder_urlto prevent SSRF attacks where a public domain resolves to a private IP address. This mitigates DNS rebinding risks and enhances security against internal network scanning.PR created automatically by Jules for task 13688478369835233394 started by @abhimehro