Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

387 lines
16 KiB
Python

"""
Tests for security/ip_ranges.py
Tests cover:
- PRIVATE_IP_RANGES constant contains expected networks
- Private IP detection works correctly
- IPv4 and IPv6 address validation
"""
import ipaddress
def _ip_is_private(ip_str: str) -> bool:
"""Module-level helper used across the IPv6-transition-prefix test
classes. (TestPrivateIPDetection has its own copy that returns
False for invalid IPs; this one is for callers that pass only
well-formed addresses.)"""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
ip = ipaddress.ip_address(ip_str)
return any(ip in network for network in PRIVATE_IP_RANGES)
class TestPrivateIPRanges:
"""Tests for PRIVATE_IP_RANGES constant."""
def test_private_ip_ranges_is_list(self):
"""PRIVATE_IP_RANGES should be a list."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
assert isinstance(PRIVATE_IP_RANGES, list)
def test_private_ip_ranges_not_empty(self):
"""PRIVATE_IP_RANGES should not be empty."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
assert len(PRIVATE_IP_RANGES) > 0
def test_private_ip_ranges_contains_ip_networks(self):
"""All entries should be ip_network objects."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
for network in PRIVATE_IP_RANGES:
assert isinstance(
network, (ipaddress.IPv4Network, ipaddress.IPv6Network)
)
def test_contains_loopback_ipv4(self):
"""Should contain IPv4 loopback (127.0.0.0/8)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
loopback = ipaddress.ip_network("127.0.0.0/8")
assert loopback in PRIVATE_IP_RANGES
def test_contains_loopback_ipv6(self):
"""Should contain IPv6 loopback (::1/128)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
loopback = ipaddress.ip_network("::1/128")
assert loopback in PRIVATE_IP_RANGES
def test_contains_rfc1918_class_a(self):
"""Should contain RFC1918 Class A private (10.0.0.0/8)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
private_a = ipaddress.ip_network("10.0.0.0/8")
assert private_a in PRIVATE_IP_RANGES
def test_contains_rfc1918_class_b(self):
"""Should contain RFC1918 Class B private (172.16.0.0/12)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
private_b = ipaddress.ip_network("172.16.0.0/12")
assert private_b in PRIVATE_IP_RANGES
def test_contains_rfc1918_class_c(self):
"""Should contain RFC1918 Class C private (192.168.0.0/16)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
private_c = ipaddress.ip_network("192.168.0.0/16")
assert private_c in PRIVATE_IP_RANGES
def test_contains_cgnat(self):
"""Should contain CGNAT range (100.64.0.0/10)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
cgnat = ipaddress.ip_network("100.64.0.0/10")
assert cgnat in PRIVATE_IP_RANGES
def test_contains_link_local_ipv4(self):
"""Should contain IPv4 link-local (169.254.0.0/16)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
link_local = ipaddress.ip_network("169.254.0.0/16")
assert link_local in PRIVATE_IP_RANGES
def test_contains_link_local_ipv6(self):
"""Should contain IPv6 link-local (fe80::/10)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
link_local = ipaddress.ip_network("fe80::/10")
assert link_local in PRIVATE_IP_RANGES
def test_contains_ipv6_unique_local(self):
"""Should contain IPv6 unique local (fc00::/7)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
unique_local = ipaddress.ip_network("fc00::/7")
assert unique_local in PRIVATE_IP_RANGES
def test_contains_ipv4_unspecified(self):
"""Should contain 0.0.0.0/8 ('this' network — IPv4 unspecified).
Linux routes connect() to 0.0.0.0 to local host."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
unspecified_v4 = ipaddress.ip_network("0.0.0.0/8")
assert unspecified_v4 in PRIVATE_IP_RANGES
def test_contains_ipv6_unspecified(self):
"""Should contain ::/128 (IPv6 unspecified). Linux routes
connect() to [::] to local host (same semantics as 0.0.0.0).
Added in PR #3873 after empirical bypass discovery."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
unspecified_v6 = ipaddress.ip_network("::/128")
assert unspecified_v6 in PRIVATE_IP_RANGES
def test_contains_6to4_prefix(self):
"""Should contain 2002::/16 (6to4 transition prefix). Wraps
private IPv4 destinations on hosts with sit0 routes."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
sixto4 = ipaddress.ip_network("2002::/16")
assert sixto4 in PRIVATE_IP_RANGES
def test_contains_nat64_prefix(self):
"""Should contain 64:ff9b::/96 (NAT64 well-known prefix)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
nat64 = ipaddress.ip_network("64:ff9b::/96")
assert nat64 in PRIVATE_IP_RANGES
def test_contains_nat64_local_use_prefix(self):
"""Should contain 64:ff9b:1::/48 (RFC 8215 NAT64 local-use prefix)
— same SSRF threat class as the well-known prefix; missing it is
the exact bypass paid out as a HackerOne bounty against
ssrf_filter."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
nat64_local = ipaddress.ip_network("64:ff9b:1::/48")
assert nat64_local in PRIVATE_IP_RANGES
def test_nat64_prefixes_constant_exposes_both(self):
"""NAT64_PREFIXES must contain exactly the two NAT64 prefixes —
used by validators to identify which deny entries the
security.allow_nat64 env carve-out should skip."""
from local_deep_research.security.ip_ranges import NAT64_PREFIXES
assert ipaddress.ip_network("64:ff9b::/96") in NAT64_PREFIXES
assert ipaddress.ip_network("64:ff9b:1::/48") in NAT64_PREFIXES
assert len(NAT64_PREFIXES) == 2
def test_contains_teredo_prefix(self):
"""Should contain 2001::/32 (Teredo)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
teredo = ipaddress.ip_network("2001::/32")
assert teredo in PRIVATE_IP_RANGES
def test_contains_ipv6_discard_prefix(self):
"""Should contain 100::/64 (RFC 6666 discard)."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
discard = ipaddress.ip_network("100::/64")
assert discard in PRIVATE_IP_RANGES
def test_contains_ipv4_compatible_ipv6_prefix(self):
"""Should contain ::/96 (RFC 4291 IPv4-Compatible IPv6 — DEPRECATED).
Same SSRF threat class as the transition prefixes: embeds an IPv4
address in the low 32 bits and is routable on hosts with ::/96
routes configured. [::169.254.169.254] would otherwise reach IMDS."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
ipv4_compat = ipaddress.ip_network("::/96")
assert ipv4_compat in PRIVATE_IP_RANGES
class TestPrivateIPDetection:
"""Tests for using PRIVATE_IP_RANGES to detect private IPs."""
def _is_private(self, ip_str: str) -> bool:
"""Helper to check if IP is in private ranges."""
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
try:
ip = ipaddress.ip_address(ip_str)
return any(ip in network for network in PRIVATE_IP_RANGES)
except ValueError:
return False
def test_localhost_is_private(self):
"""127.0.0.1 should be detected as private."""
assert self._is_private("127.0.0.1") is True
def test_localhost_ipv6_is_private(self):
"""::1 should be detected as private."""
assert self._is_private("::1") is True
def test_10_network_is_private(self):
"""10.x.x.x addresses should be detected as private."""
assert self._is_private("10.0.0.1") is True
assert self._is_private("10.255.255.255") is True
def test_172_16_network_is_private(self):
"""172.16-31.x.x addresses should be detected as private."""
assert self._is_private("172.16.0.1") is True
assert self._is_private("172.31.255.255") is True
def test_172_32_is_not_private(self):
"""172.32.x.x should NOT be detected as private."""
assert self._is_private("172.32.0.1") is False
def test_192_168_network_is_private(self):
"""192.168.x.x addresses should be detected as private."""
assert self._is_private("192.168.0.1") is True
assert self._is_private("192.168.255.255") is True
def test_cgnat_is_private(self):
"""100.64.x.x CGNAT addresses should be detected as private."""
assert self._is_private("100.64.0.1") is True
assert self._is_private("100.127.255.255") is True
def test_link_local_is_private(self):
"""169.254.x.x link-local addresses should be detected as private."""
assert self._is_private("169.254.1.1") is True
def test_public_ip_is_not_private(self):
"""Public IPs should NOT be detected as private."""
assert self._is_private("8.8.8.8") is False
assert self._is_private("1.1.1.1") is False
assert self._is_private("93.184.216.34") is False # example.com
def test_ipv6_public_is_not_private(self):
"""Public IPv6 addresses should NOT be detected as private."""
assert self._is_private("2001:4860:4860::8888") is False # Google DNS
class TestIPv6TransitionPrefixesAntiCollision:
"""Anti-regression: confirm the new IPv6 transition-prefix entries
do NOT swallow legitimate global IPv6 allocations at their boundaries.
Each prefix is precisely scoped at the bit level:
- 2001::/32 fixes the second hextet to 0x0000 (Teredo only).
- 2002::/16 fixes the first hextet to 0x2002 (6to4 only).
- 64:ff9b::/96 fixes the first 96 bits (the well-known NAT64 prefix
only; RFC 8215 local-use 64:ff9b:1::/48 must NOT match).
- 100::/64 fixes the first 64 bits (RFC 6666 discard only; the rest
of 100::/8 is unallocated, not discard).
"""
_is_private = staticmethod(_ip_is_private)
def test_google_dns_v6_not_blocked(self):
"""2001:4860:4860::8888 — second hextet 0x4860, outside 2001::/32."""
assert self._is_private("2001:4860:4860::8888") is False
def test_cloudflare_dns_v6_not_blocked(self):
"""2606:4700:4700::1111 — first hextet 0x2606, far from 2001/2002."""
assert self._is_private("2606:4700:4700::1111") is False
def test_documentation_prefix_v6_not_blocked(self):
"""2001:db8::/32 (RFC 3849) — second hextet 0x0db8, outside Teredo."""
assert self._is_private("2001:db8::1") is False
def test_root_server_v6_not_blocked(self):
"""2001:500::/30 (root-server allocation) — second hextet 0x0500."""
assert self._is_private("2001:500:88::1") is False
def test_he_tunnelbroker_v6_not_blocked(self):
"""2001:470::/32 (Hurricane Electric) — second hextet 0x0470."""
assert self._is_private("2001:470:1f04::1") is False
def test_neighbor_above_6to4_not_blocked(self):
"""2003::/16 (Deutsche Telekom) sits adjacent to 2002::/16."""
assert self._is_private("2003::1") is False
def test_neighbor_below_6to4_not_blocked(self):
"""2001:ffff::1 — last address of 2001:: space, not in 2002::/16."""
assert self._is_private("2001:ffff::1") is False
# NOTE: RFC 8215's 64:ff9b:1::/48 (NAT64 local-use) IS now blocked —
# see TestPrivateIPRanges::test_contains_nat64_local_use_prefix. It
# is the same SSRF threat class as the well-known /96 and missing
# it has been paid out as a HackerOne bounty against ssrf_filter.
def test_ipv6_discard_prefix_neighbor_not_blocked(self):
"""100:1::/64 — second hextet 0x0001, outside the /64 discard
block. The surrounding 100::/8 is reserved-unallocated, not
discard, so we must not over-block it."""
assert self._is_private("100:1::1") is False
class TestIPv6TransitionPrefixesPositiveDetection:
"""Confirm the new transition prefixes detect their full address space,
including embedded private-IPv4 wraps relevant to SSRF."""
_is_private = staticmethod(_ip_is_private)
def test_6to4_wraps_loopback(self):
"""[2002:7f00:1::] — 6to4 wrap of 127.0.0.1."""
assert self._is_private("2002:7f00:1::") is True
def test_6to4_wraps_rfc1918_class_a(self):
"""[2002:0a00:1::] — 6to4 wrap of 10.0.0.1."""
assert self._is_private("2002:0a00:1::") is True
def test_6to4_wraps_rfc1918_class_b(self):
"""[2002:ac10:1::] — 6to4 wrap of 172.16.0.1."""
assert self._is_private("2002:ac10:1::") is True
def test_6to4_wraps_rfc1918_class_c(self):
"""[2002:c0a8:101::] — 6to4 wrap of 192.168.1.1."""
assert self._is_private("2002:c0a8:101::") is True
def test_6to4_wraps_aws_metadata(self):
"""[2002:a9fe:a9fe::] — 6to4 wrap of 169.254.169.254 (AWS IMDS).
High-value SSRF target; must be caught by the prefix block."""
assert self._is_private("2002:a9fe:a9fe::") is True
def test_6to4_upper_boundary(self):
"""Last address in 2002::/16."""
assert (
self._is_private("2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff") is True
)
def test_nat64_wraps_loopback(self):
"""[64:ff9b::7f00:1] — NAT64 wrap of 127.0.0.1."""
assert self._is_private("64:ff9b::7f00:1") is True
def test_nat64_wraps_rfc1918_class_a(self):
"""[64:ff9b::a00:1] — NAT64 wrap of 10.0.0.1."""
assert self._is_private("64:ff9b::a00:1") is True
def test_nat64_wraps_rfc1918_class_b(self):
"""[64:ff9b::ac10:1] — NAT64 wrap of 172.16.0.1."""
assert self._is_private("64:ff9b::ac10:1") is True
def test_nat64_wraps_aws_metadata(self):
"""[64:ff9b::a9fe:a9fe] — NAT64 wrap of 169.254.169.254 (AWS IMDS)."""
assert self._is_private("64:ff9b::a9fe:a9fe") is True
def test_teredo_lower_boundary(self):
"""2001:0:0:0:0:0:0:0 — first address in 2001::/32."""
assert self._is_private("2001::") is True
def test_teredo_upper_boundary(self):
"""2001:0:ffff:ffff:ffff:ffff:ffff:ffff — last address in 2001::/32."""
assert self._is_private("2001:0:ffff:ffff:ffff:ffff:ffff:ffff") is True
def test_discard_prefix_upper_boundary(self):
"""Last address in 100::/64."""
assert self._is_private("100::ffff:ffff:ffff:ffff") is True
class TestIPRangesUsedByValidators:
"""Tests to verify PRIVATE_IP_RANGES is correctly imported by validators."""
def test_ssrf_validator_uses_shared_ranges(self):
"""SSRF validator should import BLOCKED_IP_RANGES from ip_ranges."""
from local_deep_research.security.ssrf_validator import (
BLOCKED_IP_RANGES,
)
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
# Should be the same object (imported, not copied)
assert BLOCKED_IP_RANGES is PRIVATE_IP_RANGES
def test_notification_validator_uses_shared_ranges(self):
"""Notification validator should use shared ranges."""
from local_deep_research.security.notification_validator import (
NotificationURLValidator,
)
from local_deep_research.security.ip_ranges import PRIVATE_IP_RANGES
# Should be the same object
assert NotificationURLValidator.PRIVATE_IP_RANGES is PRIVATE_IP_RANGES