test: harden webui and gateway checks
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import socket
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -107,6 +108,38 @@ def test_blocks_ipv6_mapped_rfc1918():
|
||||
assert not ok
|
||||
|
||||
|
||||
def test_blocks_sampled_addresses_from_internal_networks():
|
||||
"""Property-style guard: sampled blocked CIDRs must all fail closed."""
|
||||
configure_ssrf_whitelist([])
|
||||
blocked_networks = [
|
||||
"0.0.0.0/8",
|
||||
"10.0.0.0/8",
|
||||
"100.64.0.0/10",
|
||||
"127.0.0.0/8",
|
||||
"169.254.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"::1/128",
|
||||
"fc00::/7",
|
||||
"fe80::/10",
|
||||
]
|
||||
samples: list[str] = []
|
||||
for cidr in blocked_networks:
|
||||
network = ipaddress.ip_network(cidr)
|
||||
samples.append(str(network.network_address))
|
||||
if network.num_addresses > 2:
|
||||
samples.append(str(network.network_address + 1))
|
||||
samples.append(str(network[-2]))
|
||||
|
||||
for idx, ip in enumerate(samples):
|
||||
host = f"internal-{idx}.example"
|
||||
resolver = _fake_resolve_v6 if ":" in ip else _fake_resolve
|
||||
with patch("nanobot.security.network.socket.getaddrinfo", resolver(host, [ip])):
|
||||
ok, err = validate_url_target(f"http://{host}/")
|
||||
assert not ok, f"expected {ip} to be blocked"
|
||||
assert "blocked" in err.lower() or "private" in err.lower()
|
||||
|
||||
|
||||
def test_allows_public_ipv6():
|
||||
"""Public IPv6 addresses must still be allowed."""
|
||||
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve_v6("example.com", ["2606:4700::6810:84e5"])):
|
||||
|
||||
@@ -53,6 +53,38 @@ def test_resolve_allowed_path_blocks_parent_traversal(tmp_path: Path) -> None:
|
||||
resolve_allowed_path("../secret.txt", workspace=workspace, allowed_root=workspace)
|
||||
|
||||
|
||||
def test_resolve_allowed_path_blocks_traversal_shapes(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "workspace"
|
||||
workspace.mkdir()
|
||||
outside = tmp_path / "secret.txt"
|
||||
outside.write_text("secret", encoding="utf-8")
|
||||
|
||||
traversal_shapes: list[str | Path] = [
|
||||
"../secret.txt",
|
||||
"src/../../secret.txt",
|
||||
Path("..") / "secret.txt",
|
||||
workspace / "src" / ".." / ".." / "secret.txt",
|
||||
]
|
||||
if os.name == "nt":
|
||||
traversal_shapes.append("src\\..\\..\\secret.txt")
|
||||
|
||||
for candidate in traversal_shapes:
|
||||
with pytest.raises(WorkspaceBoundaryError, match="outside allowed directory"):
|
||||
resolve_allowed_path(candidate, workspace=workspace, allowed_root=workspace)
|
||||
|
||||
|
||||
def test_resolve_allowed_path_blocks_prefix_sibling(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "workspace"
|
||||
workspace.mkdir()
|
||||
sibling = tmp_path / "workspace-other"
|
||||
sibling.mkdir()
|
||||
secret = sibling / "secret.txt"
|
||||
secret.write_text("secret", encoding="utf-8")
|
||||
|
||||
with pytest.raises(WorkspaceBoundaryError, match="outside allowed directory"):
|
||||
resolve_allowed_path(secret, workspace=workspace, allowed_root=workspace)
|
||||
|
||||
|
||||
def test_resolve_allowed_path_blocks_symlink_escape(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "workspace"
|
||||
workspace.mkdir()
|
||||
|
||||
Reference in New Issue
Block a user