fix(security): normalize IPv6-mapped IPv4 in loopback check, add tests

- Apply _normalize_addr in _is_allowed_loopback_target so
  ::ffff:127.0.0.1 is correctly identified as loopback
- Add test for contains_internal_url with IPv6-mapped addresses
- Add test for whitelist + IPv6-mapped CGNAT interaction
This commit is contained in:
chengyongru 2026-05-30 01:49:08 +08:00 committed by Xubin Ren
parent 13dec9d2c2
commit 288146315e
2 changed files with 23 additions and 2 deletions

View File

@ -149,7 +149,7 @@ def _is_allowed_loopback_target(
hostname: str,
addrs: list[ipaddress.IPv4Address | ipaddress.IPv6Address],
) -> bool:
if not addrs or not all(addr.is_loopback for addr in addrs):
if not addrs or not all(_normalize_addr(addr).is_loopback for addr in addrs):
return False
normalized = hostname.rstrip(".").lower()
if normalized == "localhost":

View File

@ -7,7 +7,11 @@ from unittest.mock import patch
import pytest
from nanobot.security.network import configure_ssrf_whitelist, contains_internal_url, validate_url_target
from nanobot.security.network import (
configure_ssrf_whitelist,
contains_internal_url,
validate_url_target,
)
def _fake_resolve(host: str, results: list[str]):
@ -155,6 +159,12 @@ def test_loopback_exception_rejects_metadata():
assert contains_internal_url("curl http://169.254.169.254/latest/meta-data/", allow_loopback=True)
def test_detects_ipv6_mapped_loopback():
"""contains_internal_url must catch IPv6-mapped loopback in shell commands."""
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve_v6("evil.com", ["::ffff:127.0.0.1"])):
assert contains_internal_url("curl http://evil.com/secret")
def test_allows_normal_curl():
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve("example.com", ["93.184.216.34"])):
assert not contains_internal_url("curl https://example.com/api/data")
@ -206,3 +216,14 @@ def test_whitelist_invalid_cidr_ignored():
assert ok
finally:
configure_ssrf_whitelist([])
def test_whitelist_allows_ipv6_mapped_cgnat():
"""Whitelist must work when DNS returns IPv6-mapped CGNAT address."""
configure_ssrf_whitelist(["100.64.0.0/10"])
try:
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve_v6("ts.local", ["::ffff:100.100.1.1"])):
ok, err = validate_url_target("http://ts.local/api")
assert ok, f"Whitelisted IPv6-mapped CGNAT should be allowed, got: {err}"
finally:
configure_ssrf_whitelist([])