π‘οΈ Sentinel: [HIGH] Fix SSRF via DNS resolution check#92
π‘οΈ Sentinel: [HIGH] Fix SSRF via DNS resolution check#92google-labs-jules[bot] wants to merge 1 commit intomainfrom
Conversation
Resolves domain names in `validate_folder_url` to ensure they do not point to private or loopback IP addresses. This mitigates SSRF attacks where a user-provided domain could target internal services. - Uses `socket.getaddrinfo` to support both IPv4 and IPv6. - Checks all resolved addresses against private/loopback ranges. - Fails closed if DNS resolution fails. - Adds comprehensive tests in `tests/test_ssrf.py`.
|
π 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
|
| except ValueError: | ||
| # Not an IP literal, it's a domain. | ||
| pass | ||
| # Not an IP literal, it's a domain. Resolve it to prevent SSRF against internal services. |
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (101/100) Warning
| addr_info = socket.getaddrinfo(hostname, None) | ||
| for res in addr_info: | ||
| # res is (family, type, proto, canonname, sockaddr) | ||
| # sockaddr is (address, port) for IPv4 and (address, port, flow info, scope id) for IPv6 |
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (108/100) Warning
| ip_str = res[4][0] | ||
| ip_obj = ipaddress.ip_address(ip_str) | ||
| if ip_obj.is_private or ip_obj.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 Exception as e: | ||
| log.warning(f"Skipping unsafe URL (DNS resolution failed): {sanitize_for_log(url)} ({e})") |
Check warning
Code scanning / Pylint (reported by Codacy)
Line too long (106/100) Warning
| @@ -0,0 +1,70 @@ | |||
| import unittest | |||
Check warning
Code scanning / Pylint (reported by Codacy)
Missing module docstring Warning test
| # Add parent directory to path to import main | ||
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
|
|
||
| from main import validate_folder_url |
Check warning
Code scanning / Pylint (reported by Codacy)
Import "from main import validate_folder_url" should be placed at the top of the module Warning test
|
|
||
| from main import validate_folder_url | ||
|
|
||
| class TestSSRF(unittest.TestCase): |
Check warning
Code scanning / Pylint (reported by Codacy)
Missing class docstring Warning test
| ip_str = res[4][0] | ||
| ip_obj = ipaddress.ip_address(ip_str) | ||
| if ip_obj.is_private or ip_obj.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 Exception as e: | ||
| log.warning(f"Skipping unsafe URL (DNS resolution failed): {sanitize_for_log(url)} ({e})") |
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
| # Add parent directory to path to import main | ||
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
|
|
||
| from main import validate_folder_url |
Check warning
Code scanning / Prospector (reported by Codacy)
Import "from main import validate_folder_url" should be placed at the top of the module (wrong-import-position) Warning test
|
|
||
| self.assertFalse(validate_folder_url(url), "Should reject domain that fails resolution") | ||
|
|
||
| if __name__ == '__main__': |
Check warning
Code scanning / Prospector (reported by Codacy)
expected 2 blank lines after class or function definition, found 1 (E305) Warning test
| except ValueError: | ||
| # Not an IP literal, it's a domain. | ||
| pass | ||
| # Not an IP literal, it's a domain. Resolve it to prevent SSRF against internal services. |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Line too long (101/100) Warning
| pass | ||
| # Not an IP literal, it's a domain. Resolve it to prevent SSRF against internal services. | ||
| try: | ||
| # Use getaddrinfo to support both IPv4 and IPv6 and check all resolved addresses |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "e" doesn't conform to snake_case naming style Warning
| addr_info = socket.getaddrinfo(hostname, None) | ||
| for res in addr_info: | ||
| # res is (family, type, proto, canonname, sockaddr) | ||
| # sockaddr is (address, port) for IPv4 and (address, port, flow info, scope id) for IPv6 |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Line too long (108/100) Warning
| ip_str = res[4][0] | ||
| ip_obj = ipaddress.ip_address(ip_str) | ||
| if ip_obj.is_private or ip_obj.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 Exception as e: | ||
| log.warning(f"Skipping unsafe URL (DNS resolution failed): {sanitize_for_log(url)} ({e})") |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Line too long (106/100) Warning
| # Add parent directory to path to import main | ||
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
|
|
||
| from main import validate_folder_url |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Import "from main import validate_folder_url" should be placed at the top of the module Warning test
|
|
||
| from main import validate_folder_url | ||
|
|
||
| class TestSSRF(unittest.TestCase): |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Missing class docstring Warning test
| pass | ||
| # Not an IP literal, it's a domain. Resolve it to prevent SSRF against internal services. | ||
| try: | ||
| # Use getaddrinfo to support both IPv4 and IPv6 and check all resolved addresses |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Catching too general exception Exception Note
| ip_str = res[4][0] | ||
| ip_obj = ipaddress.ip_address(ip_str) | ||
| if ip_obj.is_private or ip_obj.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 Exception as e: | ||
| log.warning(f"Skipping unsafe URL (DNS resolution failed): {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. |
π¨ Severity: HIGH
π‘ Vulnerability: SSRF (Server-Side Request Forgery) via
folder-urlparameter.π― Impact: Attackers could potentially access internal services or the loopback interface by providing a domain that resolves to a private IP (e.g.
local.attacker.com->127.0.0.1).π§ Fix: Added DNS resolution step in URL validation. The system now resolves the hostname and verifies that none of the resulting IPs are private or loopback addresses.
β Verification: Added
tests/test_ssrf.pywhich mocks DNS responses to verify that private IPs (IPv4 and IPv6) are rejected, while public IPs are accepted.PR created automatically by Jules for task 2009760983712888116 started by @abhimehro