From e45e3e6401347b149845f997d331dfe62cf0697c Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 5 Sep 2026 22:10:59 +0200 Subject: [PATCH] fix: reject multicast and reserved egress addresses Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb --- src/sandboxer/extensions/egress.py | 3 ++- tests/test_egress.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/sandboxer/extensions/egress.py b/src/sandboxer/extensions/egress.py index 84f2bdf..c11c591 100644 --- a/src/sandboxer/extensions/egress.py +++ b/src/sandboxer/extensions/egress.py @@ -25,7 +25,8 @@ def destinations(entries: list[str]) -> frozenset[str]: def connect_public(host: str) -> socket.socket: addresses = socket.getaddrinfo(host, 443, type=socket.SOCK_STREAM) - if not addresses or any(not ipaddress.ip_address(a[4][0]).is_global for a in addresses): + ips = [ipaddress.ip_address(a[4][0]) for a in addresses] + if not ips or any(not ip.is_global or ip.is_multicast or ip.is_reserved for ip in ips): raise ValueError("non-public destination") # Connect to the already checked numeric address; never resolve a second time. for family, kind, proto, _, address in addresses: diff --git a/tests/test_egress.py b/tests/test_egress.py index e36cb89..8656173 100644 --- a/tests/test_egress.py +++ b/tests/test_egress.py @@ -24,7 +24,8 @@ def test_invalid_destination(entry): @pytest.mark.parametrize( - "address", ["127.0.0.1", "10.0.0.1", "169.254.169.254", "::1", "::ffff:127.0.0.1"] + "address", + ["127.0.0.1", "10.0.0.1", "169.254.169.254", "::1", "::ffff:127.0.0.1", "224.0.0.1", "ff02::1"], ) def test_nonpublic_dns_refused(address): with ( @@ -106,10 +107,13 @@ def test_profile_cannot_expand_owner_allowlist(tmp_path): def test_failed_broker_readiness_removes_egress(): from sandboxer.extensions.bwrap import BwrapExtension + ext = BwrapExtension() handle = {"egress_pid": "123"} - with patch.object(ext, "_wait_ready", side_effect=RuntimeError("startup failed")), \ - patch.object(ext, "teardown") as cleanup: + with ( + patch.object(ext, "_wait_ready", side_effect=RuntimeError("startup failed")), + patch.object(ext, "teardown") as cleanup, + ): with pytest.raises(RuntimeError, match="startup failed"): ext.wait_ready(handle) cleanup.assert_called_once_with(handle)