added pytest test suite (174 tests)
174 tests covering URL normalization, FTS5 query sanitization, SSRF/CSRF guards, sharing-mode logic, DB schema and upsert paths, handler end-to-end flows, and gateway body-size / mesh-whitelist guards. Each recent bug-fix commit (6ffd38d,1bc695f,8dffd8c) has an explicit regression test in test_regressions.py. One xfail documents a minor latent bug in clean_url where port 80 is not stripped from upgraded https URLs.
This commit is contained in:
parent
55c6619ba3
commit
4d522ce62c
18 changed files with 1673 additions and 0 deletions
64
tests/test_ssrf.py
Normal file
64
tests/test_ssrf.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"""Tests for `_validate_url_target` — SSRF prevention.
|
||||
|
||||
Any URL the app fetches must resolve to a public IP; private/internal/
|
||||
loopback addresses must be rejected so attacker-controlled URLs cannot
|
||||
reach internal services via our HTTP client.
|
||||
"""
|
||||
import socket
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from db import _validate_url_target
|
||||
|
||||
|
||||
def _mock_getaddrinfo(address):
|
||||
"""Return a function suitable as a socket.getaddrinfo replacement."""
|
||||
def f(host, port, *args, **kwargs):
|
||||
family = socket.AF_INET6 if ":" in address else socket.AF_INET
|
||||
return [(family, socket.SOCK_STREAM, 0, "", (address, port or 80))]
|
||||
return f
|
||||
|
||||
|
||||
@pytest.mark.parametrize("blocked_ip", [
|
||||
"127.0.0.1",
|
||||
"127.1.2.3",
|
||||
"10.0.0.1",
|
||||
"10.255.255.255",
|
||||
"172.16.0.1",
|
||||
"172.31.255.255",
|
||||
"192.168.0.1",
|
||||
"192.168.255.255",
|
||||
"169.254.169.254",
|
||||
"0.0.0.0",
|
||||
"::1",
|
||||
"fc00::1",
|
||||
"fe80::1",
|
||||
])
|
||||
def test_blocks_private_and_loopback(monkeypatch, blocked_ip):
|
||||
monkeypatch.setattr(socket, "getaddrinfo", _mock_getaddrinfo(blocked_ip))
|
||||
with pytest.raises(ValueError, match="blocked"):
|
||||
_validate_url_target("https://evil.example.com/internal")
|
||||
|
||||
|
||||
def test_allows_public_ipv4(monkeypatch):
|
||||
monkeypatch.setattr(socket, "getaddrinfo", _mock_getaddrinfo("8.8.8.8"))
|
||||
_validate_url_target("https://dns.example.com/") # does not raise
|
||||
|
||||
|
||||
def test_allows_public_ipv6(monkeypatch):
|
||||
monkeypatch.setattr(socket, "getaddrinfo", _mock_getaddrinfo("2001:4860:4860::8888"))
|
||||
_validate_url_target("https://v6.example.com/") # does not raise
|
||||
|
||||
|
||||
def test_rejects_unresolvable_hostname(monkeypatch):
|
||||
def boom(*args, **kwargs):
|
||||
raise socket.gaierror("no such host")
|
||||
monkeypatch.setattr(socket, "getaddrinfo", boom)
|
||||
with pytest.raises(ValueError, match="Cannot resolve"):
|
||||
_validate_url_target("https://does-not-exist.example.com/")
|
||||
|
||||
|
||||
def test_rejects_missing_hostname():
|
||||
with pytest.raises(ValueError, match="No hostname"):
|
||||
_validate_url_target("http:///path-only")
|
||||
Loading…
Add table
Add a link
Reference in a new issue