diff --git a/src/paperless/network.py b/src/paperless/network.py index 8a5b28a91..38b2c852e 100644 --- a/src/paperless/network.py +++ b/src/paperless/network.py @@ -6,6 +6,17 @@ from urllib.parse import urlparse import httpx +# Ranges ipaddress does not report as private, but which routinely front +# internal infrastructure. +_NON_PUBLIC_NETWORKS = ( + # RFC 6598 shared address space: ISP CGNAT, and the default pod/service + # CIDR on several managed Kubernetes offerings. + ipaddress.ip_network("100.64.0.0/10"), + # RFC 6052 NAT64 well-known prefix: 64:ff9b::7f00:1 is 127.0.0.1 wherever + # a NAT64 gateway exists. + ipaddress.ip_network("64:ff9b::/96"), +) + def is_public_ip(ip: str | int) -> bool: try: @@ -16,6 +27,7 @@ def is_public_ip(ip: str | int) -> bool: or obj.is_link_local or obj.is_multicast or obj.is_unspecified + or any(obj in network for network in _NON_PUBLIC_NETWORKS) ) except ValueError: # pragma: no cover return False diff --git a/src/paperless/tests/test_network.py b/src/paperless/tests/test_network.py index a306c52b4..3fca931fe 100644 --- a/src/paperless/tests/test_network.py +++ b/src/paperless/tests/test_network.py @@ -4,6 +4,7 @@ import httpx import pytest from paperless.network import PinnedHostHTTPTransport +from paperless.network import is_public_ip def test_pinned_host_transport_blocks_internal_rebinding(): @@ -48,3 +49,42 @@ def test_pinned_host_transport_rewrites_to_vetted_ip(): response = transport.handle_request(request) assert response.status_code == 200 + + +@pytest.mark.parametrize( + "address", + [ + "127.0.0.1", + "10.0.0.5", + "169.254.169.254", + "::1", + "fc00::1", + "fe80::1", + # RFC 6598 shared address space, incl. both edges of the /10 + "100.64.0.0", + "100.64.0.1", + "100.127.255.255", + # RFC 6052 NAT64 well-known prefix, embedding 127.0.0.1 and 10.0.0.5 + "64:ff9b::7f00:1", + "64:ff9b::a00:5", + ], +) +def test_is_public_ip_blocks_non_public_addresses(address): + assert not is_public_ip(address) + + +@pytest.mark.parametrize( + "address", + [ + "8.8.8.8", + "142.250.185.196", + "2606:4700:4700::1111", + # just outside the ranges above, must stay reachable + "100.63.255.255", + "100.128.0.0", + "64:ff9a::1", + "64:ff9c::1", + ], +) +def test_is_public_ip_allows_public_addresses(address): + assert is_public_ip(address)