Skip to content

🛡️ Sentinel: Upgrade SSRF protection to block non-global and multicast IPs#115

Merged
abhimehro merged 1 commit intomainfrom
sentinel-ssrf-upgrade-12310260580531266516
Jan 19, 2026
Merged

🛡️ Sentinel: Upgrade SSRF protection to block non-global and multicast IPs#115
abhimehro merged 1 commit intomainfrom
sentinel-ssrf-upgrade-12310260580531266516

Conversation

@google-labs-jules
Copy link

🛡️ Sentinel Security Update

🚨 Vulnerability Fix

Upgraded Server-Side Request Forgery (SSRF) protection in validate_folder_url.

The previous implementation used ip.is_private which allows several non-public IP ranges:

  • Carrier-Grade NAT (CGNAT): 100.64.0.0/10
  • Link-Local: 169.254.0.0/16 (in some contexts)
  • Reserved: 240.0.0.0/4, etc.
  • Multicast: 224.0.0.0/4

🔧 The Fix

Switched to using ip.is_global (introduced in Python 3.4), which correctly identifies addresses allocated for public networks.

if not ip.is_global or ip.is_multicast:
    return False

Note: We explicitly block is_multicast because is_global considers multicast addresses as global.

✅ Verification

Added new tests in tests/test_ssrf_enhanced.py that specifically target these previously allowed ranges. All tests passed.

📦 Dependency Note

Updated .python-version to 3.13 to align with pyproject.toml's requires-python = ">=3.13".


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

…t IPs

Fixes a gap in SSRF protection where Carrier-Grade NAT (100.64.0.0/10), Link-Local, and Reserved IP ranges were allowed because `is_private` does not cover them.

Changes:
- Replaced `ip.is_private or ip.is_loopback` with `not ip.is_global or ip.is_multicast`.
- Added `tests/test_ssrf_enhanced.py` to verify blocking of CGNAT and Multicast addresses.
- Updated `.python-version` to 3.13 to match `pyproject.toml` requirements and support modern `ipaddress` features.
@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.

@trunk-io
Copy link

trunk-io bot commented Jan 19, 2026

😎 Merged manually by Abhi Mehrotra (@abhimehro) - details.

2. Check all returned IPs against private and loopback ranges.
3. Fail closed (block the URL) if resolution fails or returns any private IP.

## 2026-03-22 - [SSRF Protection Gaps in Python ipaddress]

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
2. Check all returned IPs against private and loopback ranges.
3. Fail closed (block the URL) if resolution fails or returns any private IP.

## 2026-03-22 - [SSRF Protection Gaps in Python ipaddress]

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
with patch('socket.getaddrinfo') as mock_getaddrinfo:
# Simulate resolving to 0.0.0.0
mock_getaddrinfo.return_value = [
(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('0.0.0.0', 443))

Check notice

Code scanning / Bandit (reported by Codacy)

Possible binding to all interfaces. Note test

Possible binding to all interfaces.
if ip.is_private or ip.is_loopback:
log.warning(f"Skipping unsafe URL (private IP): {sanitize_for_log(url)}")
if not ip.is_global or ip.is_multicast:
log.warning(f"Skipping unsafe URL (non-global/multicast IP): {sanitize_for_log(url)}")

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (102/100) Warning

Line too long (102/100)
if ip.is_private or ip.is_loopback:
log.warning(f"Skipping unsafe URL (domain {hostname} resolves to private IP {ip}): {sanitize_for_log(url)}")
if not ip.is_global or ip.is_multicast:
log.warning(f"Skipping unsafe URL (domain {hostname} resolves to non-global/multicast IP {ip}): {sanitize_for_log(url)}")

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (145/100) Warning

Line too long (145/100)
@@ -0,0 +1,58 @@

Check warning

Code scanning / Pylint (reported by Codacy)

Missing module docstring Warning test

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

import main

Check warning

Code scanning / Pylint (reported by Codacy)

Import "import main" should be placed at the top of the module Warning test

Import "import main" should be placed at the top of the module

import main

class TestSSRFEnhanced(unittest.TestCase):

Check warning

Code scanning / Pylint (reported by Codacy)

Missing class docstring Warning test

Missing class docstring
if ip.is_private or ip.is_loopback:
log.warning(f"Skipping unsafe URL (private IP): {sanitize_for_log(url)}")
if not ip.is_global or ip.is_multicast:
log.warning(f"Skipping unsafe URL (non-global/multicast 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)
if ip.is_private or ip.is_loopback:
log.warning(f"Skipping unsafe URL (domain {hostname} resolves to private IP {ip}): {sanitize_for_log(url)}")
if not ip.is_global or ip.is_multicast:
log.warning(f"Skipping unsafe URL (domain {hostname} resolves to non-global/multicast IP {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)
# Add root to path to import main
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import main

Check warning

Code scanning / Prospector (reported by Codacy)

Import "import main" should be placed at the top of the module (wrong-import-position) Warning test

Import "import main" should be placed at the top of the module (wrong-import-position)
result = main.validate_folder_url(url)
self.assertFalse(result, "Should block domain resolving to 0.0.0.0")

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

expected 2 blank lines after class or function definition, found 1 (E305)
if ip.is_private or ip.is_loopback:
log.warning(f"Skipping unsafe URL (private IP): {sanitize_for_log(url)}")
if not ip.is_global or ip.is_multicast:
log.warning(f"Skipping unsafe URL (non-global/multicast IP): {sanitize_for_log(url)}")

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (102/100) Warning

Line too long (102/100)
if ip.is_private or ip.is_loopback:
log.warning(f"Skipping unsafe URL (domain {hostname} resolves to private IP {ip}): {sanitize_for_log(url)}")
if not ip.is_global or ip.is_multicast:
log.warning(f"Skipping unsafe URL (domain {hostname} resolves to non-global/multicast IP {ip}): {sanitize_for_log(url)}")

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (145/100) Warning

Line too long (145/100)
@@ -0,0 +1,58 @@

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Missing module docstring Warning test

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

import main

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Import "import main" should be placed at the top of the module Warning test

Import "import main" should be placed at the top of the module

import main

class TestSSRFEnhanced(unittest.TestCase):

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Missing class docstring Warning test

Missing class docstring
if ip.is_private or ip.is_loopback:
log.warning(f"Skipping unsafe URL (private IP): {sanitize_for_log(url)}")
if not ip.is_global or ip.is_multicast:
log.warning(f"Skipping unsafe URL (non-global/multicast 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
if ip.is_private or ip.is_loopback:
log.warning(f"Skipping unsafe URL (domain {hostname} resolves to private IP {ip}): {sanitize_for_log(url)}")
if not ip.is_global or ip.is_multicast:
log.warning(f"Skipping unsafe URL (domain {hostname} resolves to non-global/multicast IP {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
@abhimehro abhimehro marked this pull request as ready for review January 19, 2026 18:44
Copilot AI review requested due to automatic review settings January 19, 2026 18:44
@abhimehro abhimehro merged commit 7b9d274 into main Jan 19, 2026
23 of 24 checks passed
@abhimehro abhimehro deleted the sentinel-ssrf-upgrade-12310260580531266516 branch January 19, 2026 19:02
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.

1 participant