From c792a1fdc1b2b9da889241d7f88abc3b4959d204 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:50:51 +0000 Subject: [PATCH] Fix SSRF vulnerability by resolving domains to check for private IPs --- .jules/sentinel.md | 8 ++++++++ main.py | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 675973d..3eb5065 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -39,3 +39,11 @@ **Prevention:** 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-02-17 - [SSRF DNS Rebinding Prevention] +**Vulnerability:** The `validate_folder_url` function checked for private IP literals but allowed domain names that resolved to private IPs, making the application vulnerable to SSRF via DNS rebinding or simple internal domain names. +**Learning:** Simply checking if a hostname is a private IP string is not enough. You must resolve the domain to an IP address and check the resolved IP against private ranges. +**Prevention:** +1. Resolve domains using `socket.gethostbyname` or similar. +2. Validate the resolved IP against private IP ranges using `ipaddress.is_private`. +3. Handle DNS resolution failures gracefully (fail closed). diff --git a/main.py b/main.py index e6aabc5..c0ef9bb 100644 --- a/main.py +++ b/main.py @@ -23,6 +23,7 @@ import concurrent.futures import threading import ipaddress +import socket from urllib.parse import urlparse from typing import Dict, List, Optional, Any, Set, Sequence @@ -205,12 +206,18 @@ def validate_folder_url(url: str) -> bool: try: ip = ipaddress.ip_address(hostname) - if ip.is_private or ip.is_loopback: - log.warning(f"Skipping unsafe URL (private IP): {sanitize_for_log(url)}") - return False except ValueError: - # Not an IP literal, it's a domain. - pass + # Not an IP literal, try to resolve domain + try: + ip_str = socket.gethostbyname(hostname) + ip = ipaddress.ip_address(ip_str) + except (socket.gaierror, ValueError) as e: + log.warning(f"Failed to resolve hostname for {sanitize_for_log(url)}: {e}") + return False + + if ip.is_private or ip.is_loopback: + log.warning(f"Skipping unsafe URL (resolves to private IP): {sanitize_for_log(url)} -> {ip}") + return False except Exception as e: log.warning(f"Failed to validate URL {sanitize_for_log(url)}: {e}")