From 479731d8bf915781daf38d6b9212a1cb43970c63 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:11:50 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]=20Fi?= =?UTF-8?q?x=20SSRF=20protection=20to=20explicitly=20block=20link-local=20?= =?UTF-8?q?IPs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ===== ELIR ===== PURPOSE: Explicitly blocks link-local IP addresses (e.g., 169.254.169.254) in the _is_safe_ip validation. SECURITY: Defense-in-depth to prevent SSRF bypasses via OS network stacks unpredictably routing link-local addresses, protecting metadata services. FAILS IF: An attacker tries to sync a rule URL resolving to a metadata service. VERIFY: Ensure tests/test_ssrf_link_local.py correctly blocks 169.254.169.254 and test_ssrf_loopback.py continues to block 127.0.0.1. MAINTAIN: The checks for is_loopback, is_unspecified, is_private, and is_link_local must all remain explicitly defined before the fallback to is_global. Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ main.py | 2 ++ tests/test_ssrf_link_local.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 tests/test_ssrf_link_local.py diff --git a/.jules/sentinel.md b/.jules/sentinel.md index feb373d..df14377 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -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. diff --git a/main.py b/main.py index ea44b9a..4c2676f 100644 --- a/main.py +++ b/main.py @@ -1075,6 +1075,8 @@ def _is_safe_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool: return False if ip.is_loopback: 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: diff --git a/tests/test_ssrf_link_local.py b/tests/test_ssrf_link_local.py new file mode 100644 index 0000000..fcef4f2 --- /dev/null +++ b/tests/test_ssrf_link_local.py @@ -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") + +if __name__ == "__main__": + unittest.main()