🛡️ Sentinel: Fix SSRF via DNS Resolution#103
🛡️ Sentinel: Fix SSRF via DNS Resolution#103google-labs-jules[bot] wants to merge 1 commit intomainfrom
Conversation
Enhances `validate_folder_url` to resolve hostnames to IP addresses and check them against private/loopback ranges. This prevents SSRF attacks that use public domain names pointing to internal IP addresses (e.g., DNS rebinding or simple resolution to private IPs). - Import `socket` module. - Use `socket.getaddrinfo` to resolve hostnames. - Verify resolved IPs using `ipaddress` module. - Fail validation if any resolved IP is private or loopback.
|
👋 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
|
| try: | ||
| # Use getaddrinfo to support both IPv4 and IPv6 | ||
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type, proto, canonname, sockaddr in addr_infos: |
Check warning
Code scanning / Prospector (reported by Codacy)
Redefining built-in 'type' (redefined-builtin) Warning
| try: | ||
| # Use getaddrinfo to support both IPv4 and IPv6 | ||
| 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 = 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
| log.warning(f"Skipping unsafe URL (domain resolves to private IP {ip_str}): {sanitize_for_log(url)}") | ||
| return False | ||
| except socket.gaierror: | ||
| log.warning(f"Could not resolve hostname for validation: {sanitize_for_log(hostname)}") |
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
| log.warning(f"Could not resolve hostname for validation: {sanitize_for_log(hostname)}") | ||
| return False | ||
| except Exception as e: | ||
| log.warning(f"Error during DNS resolution check: {e}") |
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
| ## 2025-01-21 - [SSRF Protection and Input Limits] | ||
| **Vulnerability:** The `folder_url` validation checked for HTTPS but allowed internal IP addresses (e.g., `127.0.0.1`, `10.0.0.0/8`). This could theoretically allow Server-Side Request Forgery (SSRF) if the script is run in an environment with access to sensitive internal services. Additionally, `profile_id` had no length limit. | ||
| **Learning:** HTTPS validation alone is insufficient to prevent SSRF against internal services that might support HTTPS or use self-signed certs (if verification was disabled or bypassed). Explicitly blocking private IP ranges provides necessary defense-in-depth. | ||
| ## 2025-01-21 - [SSRF Protection via DNS Resolution] |
Check notice
Code scanning / Remark-lint (reported by Codacy)
Warn when references to undefined definitions are found. Note
| ## 2025-01-21 - [SSRF Protection and Input Limits] | ||
| **Vulnerability:** The `folder_url` validation checked for HTTPS but allowed internal IP addresses (e.g., `127.0.0.1`, `10.0.0.0/8`). This could theoretically allow Server-Side Request Forgery (SSRF) if the script is run in an environment with access to sensitive internal services. Additionally, `profile_id` had no length limit. | ||
| **Learning:** HTTPS validation alone is insufficient to prevent SSRF against internal services that might support HTTPS or use self-signed certs (if verification was disabled or bypassed). Explicitly blocking private IP ranges provides necessary defense-in-depth. | ||
| ## 2025-01-21 - [SSRF Protection via DNS Resolution] |
Check notice
Code scanning / Remark-lint (reported by Codacy)
Warn when shortcut reference links are used. Note
| if '%' in ip_str: | ||
| ip_str = ip_str.split('%')[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 = 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
| log.warning(f"Skipping unsafe URL (domain resolves to private IP {ip_str}): {sanitize_for_log(url)}") | ||
| return False | ||
| except socket.gaierror: | ||
| log.warning(f"Could not resolve hostname for validation: {sanitize_for_log(hostname)}") |
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (103/100) Warning
| try: | ||
| # Use getaddrinfo to support both IPv4 and IPv6 | ||
| 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
| try: | ||
| # Use getaddrinfo to support both IPv4 and IPv6 | ||
| 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
| try: | ||
| # Use getaddrinfo to support both IPv4 and IPv6 | ||
| 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
| try: | ||
| # Use getaddrinfo to support both IPv4 and IPv6 | ||
| addr_infos = socket.getaddrinfo(hostname, None) | ||
| for family, type, proto, canonname, sockaddr in addr_infos: |
Check notice
Code scanning / Pylint (reported by Codacy)
Redefining built-in 'type' Note
| try: | ||
| # Use getaddrinfo to support both IPv4 and IPv6 | ||
| 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
| except socket.gaierror: | ||
| log.warning(f"Could not resolve hostname for validation: {sanitize_for_log(hostname)}") | ||
| return False | ||
| except Exception as e: |
Check notice
Code scanning / Pylint (reported by Codacy)
Catching too general exception Exception Note
| if '%' in ip_str: | ||
| ip_str = ip_str.split('%')[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 = 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
| log.warning(f"Skipping unsafe URL (domain resolves to private IP {ip_str}): {sanitize_for_log(url)}") | ||
| return False | ||
| except socket.gaierror: | ||
| log.warning(f"Could not resolve hostname for validation: {sanitize_for_log(hostname)}") |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Line too long (103/100) Warning
| except socket.gaierror: | ||
| log.warning(f"Could not resolve hostname for validation: {sanitize_for_log(hostname)}") | ||
| return False | ||
| except Exception as e: |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "e" doesn't conform to snake_case naming style Warning
| try: | ||
| # Use getaddrinfo to support both IPv4 and IPv6 | ||
| 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
| try: | ||
| # Use getaddrinfo to support both IPv4 and IPv6 | ||
| 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
|
|
||
| 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
| log.warning(f"Skipping unsafe URL (domain resolves to private IP {ip_str}): {sanitize_for_log(url)}") | ||
| return False | ||
| except socket.gaierror: | ||
| log.warning(f"Could not resolve hostname for validation: {sanitize_for_log(hostname)}") |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Use lazy % formatting in logging functions Note
| except socket.gaierror: | ||
| log.warning(f"Could not resolve hostname for validation: {sanitize_for_log(hostname)}") | ||
| return False | ||
| except Exception as e: |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Catching too general exception Exception Note
| log.warning(f"Could not resolve hostname for validation: {sanitize_for_log(hostname)}") | ||
| return False | ||
| except Exception as e: | ||
| log.warning(f"Error during DNS resolution check: {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. |
🛡️ Sentinel: [HIGH] Fix SSRF via DNS Resolution
🚨 Severity: HIGH
💡 Vulnerability: The previous validation logic checked for private IP literals but allowed domain names that resolve to private IPs (e.g.,
localtest.me->127.0.0.1), enabling SSRF attacks against internal services.🎯 Impact: An attacker could supply a malicious folder URL that points to internal network resources, potentially accessing sensitive internal APIs or metadata services.
🔧 Fix: Added DNS resolution step to
validate_folder_url. Now resolves the hostname and checks if the underlying IP is private/loopback usingsocket.getaddrinfoandipaddress.✅ Verification: Verified using a reproduction script
reproduce_ssrf.py(deleted before submit) which confirmedlocaltest.meis now blocked. Ran dry-run to ensure normal functionality is preserved.PR created automatically by Jules for task 931545182998519987 started by @abhimehro