Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
**Vulnerability:** The `_is_safe_ip` function relied primarily on `is_private` and `is_global` properties of Python's `ipaddress` module to prevent SSRF loopback connections. While these often cover `127.0.0.1` and `::1`, edge cases and alternative loopback addresses may bypass these checks depending on OS/network configurations.
**Learning:** Defense-in-depth is essential when validating IPs. Relying solely on `is_private` or `is_global` without explicitly checking `is_loopback` creates potential edge cases where loopback traffic might not be caught, increasing SSRF risk.
**Prevention:** Explicitly check for `is_loopback` along with `is_unspecified` and `is_private` to ensure comprehensive outbound SSRF filtering.

## 2025-04-07 - Add explicit link-local IP check to prevent SSRF bypass
**Vulnerability:** The `_is_safe_ip` function lacked an explicit check for link-local addresses (like `169.254.169.254`, commonly used for cloud metadata services), implicitly relying on `is_global` or `is_private`.
**Learning:** OS-specific network stacks might unexpectedly route link-local addresses or fail to properly classify them as non-global or private. This could allow attackers to bypass SSRF protections and access sensitive internal metadata endpoints.
**Prevention:** Defense-in-depth requires explicitly blocking `ip.is_link_local` alongside `is_loopback`, `is_unspecified`, and `is_private` to ensure complete outbound SSRF filtering.
2 changes: 2 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,8 @@ def _is_safe_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
return False
if ip.is_loopback:
return False
Comment on lines 1076 to 1077
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

_is_safe_ip checks ip.is_loopback twice (once earlier and again here). The second check is redundant and makes the blocklist logic harder to audit; please remove the duplicate conditional to keep the security checks unambiguous.

Suggested change
if ip.is_loopback:
return False
# Keep one authoritative loopback check so the blocklist stays easy to audit.

Copilot uses AI. Check for mistakes.
if ip.is_link_local:
return False
Comment on lines 1076 to +1079
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The is_loopback check is duplicated here. It is already performed on lines 1072-1073. Removing the redundant check improves code clarity and performance.

Suggested change
if ip.is_loopback:
return False
if ip.is_link_local:
return False
if ip.is_link_local:
return False

if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
return _is_safe_ip(ip.ipv4_mapped)
if isinstance(ip, ipaddress.IPv4Address) and ip in _CGNAT_NETWORK:
Expand Down
28 changes: 28 additions & 0 deletions tests/test_ssrf_link_local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import os
import socket
import sys
import unittest
from unittest.mock import patch

# Add root to path to import main
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import main

class TestSSRFLinkLocal(unittest.TestCase):
def test_domain_resolving_to_link_local_ip(self):
"""
Test that a domain resolving to a link-local IP (169.254.169.254) is blocked.
"""
with patch("socket.getaddrinfo") as mock_getaddrinfo:
# Simulate resolving to AWS metadata service IP
mock_getaddrinfo.return_value = [
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("169.254.169.254", 443))
]

url = "https://metadata.example.com/config.json"
result = main.validate_folder_url(url)
self.assertFalse(result, "Should block domain resolving to link-local IP")
Comment on lines +23 to +25
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

This test covers the DNS-resolution path, but the new ip.is_link_local protection also applies to IP-literal hosts (e.g., https://169.254.169.254/...) via validate_hostname’s IP parsing branch. Please add a second assertion (or test) that a URL containing a link-local IP literal is blocked, to prevent regressions in the direct-IP path.

Copilot generated this review using guidance from repository custom instructions.

if __name__ == "__main__":
unittest.main()
Loading