Skip to content

Sentinel: [HIGH] Fix SSRF via DNS resolution#75

Closed
google-labs-jules[bot] wants to merge 2 commits intomainfrom
sentinel-fix-ssrf-dns-resolution-7206426744808280296
Closed

Sentinel: [HIGH] Fix SSRF via DNS resolution#75
google-labs-jules[bot] wants to merge 2 commits intomainfrom
sentinel-fix-ssrf-dns-resolution-7206426744808280296

Conversation

@google-labs-jules
Copy link

This PR addresses a Server-Side Request Forgery (SSRF) vulnerability where the application could be tricked into accessing internal services by providing a domain name that resolves to a private IP address (e.g., local.test -> 127.0.0.1).

Previously, the validation only checked for explicit IP literals. The fix adds DNS resolution using socket.getaddrinfo and verifies that none of the resolved IPs fall into private or loopback ranges. It fails closed if DNS resolution fails.

This change enhances the security of the folder sync tool by preventing it from interacting with local network resources.


PR created automatically by Jules for task 7206426744808280296 started by @abhimehro

- Updated `validate_folder_url` in `main.py` to resolve domains using `socket.getaddrinfo`.
- Added checks to reject URLs where the domain resolves to a private or loopback IP address.
- Ensures defense-in-depth against SSRF attacks using domains pointing to internal services.
- Updated Sentinel journal with the finding.
@google-labs-jules
Copy link
Author

👋 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 @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@abhimehro abhimehro marked this pull request as ready for review January 9, 2026 00:12
Copilot AI review requested due to automatic review settings January 9, 2026 00:12
@github-actions github-actions bot added the python label Jan 9, 2026
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:

Check warning

Code scanning / Prospector (reported by Codacy)

Unused variable 'family' (unused-variable) Warning

Unused variable 'family' (unused-variable)
ip_str = sockaddr[0]
resolved_ip = ipaddress.ip_address(ip_str)
if resolved_ip.is_private or resolved_ip.is_loopback:
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to private IP {resolved_ip}): {sanitize_for_log(url)}")

Check warning

Code scanning / Prospector (reported by Codacy)

Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning

Use lazy % formatting in logging functions (logging-fstring-interpolation)
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to private IP {resolved_ip}): {sanitize_for_log(url)}")
return False
except socket.gaierror:
log.warning(f"Could not resolve hostname {sanitize_for_log(hostname)}, treating as unsafe.")

Check warning

Code scanning / Prospector (reported by Codacy)

Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning

Use lazy % formatting in logging functions (logging-fstring-interpolation)
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-24 - [SSRF via DNS Resolution]

Check notice

Code scanning / Remark-lint (reported by Codacy)

Warn when references to undefined definitions are found. Note

[no-undefined-references] Found reference to undefined definition
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-24 - [SSRF via DNS Resolution]

Check notice

Code scanning / Remark-lint (reported by Codacy)

Warn when shortcut reference links are used. Note

[no-shortcut-reference-link] Use the trailing [] on reference links
ip_str = sockaddr[0]
resolved_ip = ipaddress.ip_address(ip_str)
if resolved_ip.is_private or resolved_ip.is_loopback:
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to private IP {resolved_ip}): {sanitize_for_log(url)}")

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (159/100) Warning

Line too long (159/100)
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to private IP {resolved_ip}): {sanitize_for_log(url)}")
return False
except socket.gaierror:
log.warning(f"Could not resolve hostname {sanitize_for_log(hostname)}, treating as unsafe.")

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (108/100) Warning

Line too long (108/100)
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'proto' Note

Unused variable 'proto'
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'kind' Note

Unused variable 'kind'
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'family' Note

Unused variable 'family'
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'canonname' Note

Unused variable 'canonname'
ip_str = sockaddr[0]
resolved_ip = ipaddress.ip_address(ip_str)
if resolved_ip.is_private or resolved_ip.is_loopback:
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to private IP {resolved_ip}): {sanitize_for_log(url)}")

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (159/100) Warning

Line too long (159/100)
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to private IP {resolved_ip}): {sanitize_for_log(url)}")
return False
except socket.gaierror:
log.warning(f"Could not resolve hostname {sanitize_for_log(hostname)}, treating as unsafe.")

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (108/100) Warning

Line too long (108/100)
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'kind' Note

Unused variable 'kind'
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'family' Note

Unused variable 'family'
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'proto' Note

Unused variable 'proto'
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'canonname' Note

Unused variable 'canonname'
ip_str = sockaddr[0]
resolved_ip = ipaddress.ip_address(ip_str)
if resolved_ip.is_private or resolved_ip.is_loopback:
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to private IP {resolved_ip}): {sanitize_for_log(url)}")

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Use lazy % formatting in logging functions Note

Use lazy % formatting in logging functions
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to private IP {resolved_ip}): {sanitize_for_log(url)}")
return False
except socket.gaierror:
log.warning(f"Could not resolve hostname {sanitize_for_log(hostname)}, treating as unsafe.")

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Use lazy % formatting in logging functions Note

Use lazy % formatting in logging functions
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a Server-Side Request Forgery (SSRF) vulnerability by adding DNS resolution to the URL validation logic. Previously, attackers could bypass private IP checks by using domain names that resolve to internal addresses.

Key Changes:

  • Added DNS resolution using socket.getaddrinfo to validate that domain names don't resolve to private or loopback IP addresses
  • Enhanced fail-closed behavior to reject URLs when DNS resolution fails
  • Documented the vulnerability and fix in the security sentinel log

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
main.py Added socket import and DNS resolution logic in validate_folder_url to check resolved IPs against private/loopback ranges
.jules/sentinel.md Added security documentation entry describing the DNS resolution bypass vulnerability and prevention measures

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

# Not an IP literal, it's a domain. Resolve to check for private IPs.
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DNS resolution using socket.getaddrinfo is synchronous and blocking, which can cause performance issues. Each URL validation will block the calling thread while performing DNS lookups, which could significantly slow down the application, especially when processing multiple URLs. Consider adding a timeout parameter to prevent hanging on slow DNS servers, or document this performance characteristic if it's acceptable for the use case.

Copilot uses AI. Check for mistakes.
for family, kind, proto, canonname, sockaddr in addr_infos:
ip_str = sockaddr[0]
resolved_ip = ipaddress.ip_address(ip_str)
if resolved_ip.is_private or resolved_ip.is_loopback:
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation only checks is_private and is_loopback, but doesn't check for other potentially dangerous IP address categories. Consider also checking is_link_local (e.g., 169.254.0.0/16), is_multicast, and is_reserved to provide more comprehensive SSRF protection against access to special-use IP addresses.

Copilot uses AI. Check for mistakes.
Comment on lines +213 to +225
# Not an IP literal, it's a domain. Resolve to check for private IPs.
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:
ip_str = sockaddr[0]
resolved_ip = ipaddress.ip_address(ip_str)
if resolved_ip.is_private or resolved_ip.is_loopback:
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to private IP {resolved_ip}): {sanitize_for_log(url)}")
return False
except socket.gaierror:
log.warning(f"Could not resolve hostname {sanitize_for_log(hostname)}, treating as unsafe.")
return False
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This DNS resolution approach is vulnerable to Time-of-Check-Time-of-Use (TOCTOU) race conditions. An attacker could use DNS rebinding where the domain initially resolves to a safe IP during validation, but later resolves to a private IP when the actual request is made. While this fix improves security, be aware that it doesn't fully protect against sophisticated DNS rebinding attacks. Consider implementing additional protections such as DNS pinning (caching resolved IPs and using them for the request) or validating IPs at request time if httpx supports custom DNS resolution.

Suggested change
# Not an IP literal, it's a domain. Resolve to check for private IPs.
try:
# Resolve hostname to IPs
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:
ip_str = sockaddr[0]
resolved_ip = ipaddress.ip_address(ip_str)
if resolved_ip.is_private or resolved_ip.is_loopback:
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to private IP {resolved_ip}): {sanitize_for_log(url)}")
return False
except socket.gaierror:
log.warning(f"Could not resolve hostname {sanitize_for_log(hostname)}, treating as unsafe.")
return False
# Not an IP literal; avoid DNS-based validation here to prevent
# time-of-check vs time-of-use (TOCTOU) issues with DNS rebinding.
# Additional enforcement should be done at request time (e.g. via
# httpx transport / DNS configuration) if private IP access must be
# restricted for resolved domains.
pass

Copilot uses AI. Check for mistakes.

## 2025-01-24 - [SSRF via DNS Resolution]
**Vulnerability:** The `validate_folder_url` function performed checks on the initial hostname but did not resolve domains to their IP addresses. This allowed attackers to bypass private IP blocklists by using a custom domain (e.g., `local.test`) that resolves to a private IP (e.g., `127.0.0.1`), leading to SSRF.
**Learning:** Validating hostnames without DNS resolution is insufficient for SSRF protection. Attackers can easily map public domains to internal IPs (DNS Rebinding/Pinning).
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The term "DNS Rebinding/Pinning" is potentially confusing. DNS Rebinding is an attack technique, while DNS Pinning is a defense mechanism. Consider clarifying this to "DNS Rebinding attacks" for accuracy, as the context discusses how attackers can bypass validation by controlling DNS resolution.

Suggested change
**Learning:** Validating hostnames without DNS resolution is insufficient for SSRF protection. Attackers can easily map public domains to internal IPs (DNS Rebinding/Pinning).
**Learning:** Validating hostnames without DNS resolution is insufficient for SSRF protection. Attackers can easily map public domains to internal IPs (DNS rebinding attacks).

Copilot uses AI. Check for mistakes.
Comment on lines +223 to +224
except socket.gaierror:
log.warning(f"Could not resolve hostname {sanitize_for_log(hostname)}, treating as unsafe.")
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The socket.gaierror exception only catches DNS resolution failures, but socket.getaddrinfo can also raise other exceptions like socket.timeout or OSError. While the outer try-except block at line 227 will catch these, it's less specific and the error message won't accurately reflect a DNS resolution failure. Consider catching a broader set of socket-related exceptions (OSError, socket.timeout) alongside socket.gaierror for more specific error handling.

Suggested change
except socket.gaierror:
log.warning(f"Could not resolve hostname {sanitize_for_log(hostname)}, treating as unsafe.")
except (socket.gaierror, socket.timeout, OSError) as exc:
log.warning(f"Could not resolve hostname {sanitize_for_log(hostname)} (socket error: {exc}), treating as unsafe.")

Copilot uses AI. Check for mistakes.
@abhimehro
Copy link
Owner

Superseded: Security improvements already in main.

@abhimehro abhimehro closed this Jan 18, 2026
@abhimehro abhimehro deleted the sentinel-fix-ssrf-dns-resolution-7206426744808280296 branch January 19, 2026 05:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants