web-scraper / tests /test_security.py
Claude
Claude Sonnet 5
Add rate limiting, tests, better extraction, and HF auto-deploy
ebab0e2 unverified
Raw History Blame Contribute Delete
2.44 kB
import pytest
from app.errors import ScrapeError
from app.security import assert_url_is_safe
@pytest.mark.parametrize(
"url",
[
"http://127.0.0.1/",
"http://127.0.0.1:8000/admin",
"http://localhost/",
"http://169.254.169.254/latest/meta-data/", # cloud metadata endpoint
"http://10.0.0.5/",
"http://172.16.0.1/",
"http://192.168.1.1/",
"http://0.0.0.0/",
"http://[::1]/", # IPv6 loopback
],
)
def test_blocks_private_and_loopback_targets(url):
with pytest.raises(ScrapeError) as exc_info:
assert_url_is_safe(url)
assert exc_info.value.status_code == 400
@pytest.mark.parametrize("url", ["ftp://example.com/", "file:///etc/passwd", "javascript:alert(1)"])
def test_blocks_disallowed_schemes(url):
with pytest.raises(ScrapeError) as exc_info:
assert_url_is_safe(url)
assert exc_info.value.status_code == 400
def test_blocks_explicit_blocklisted_hostname():
with pytest.raises(ScrapeError):
assert_url_is_safe("http://metadata.google.internal/")
def test_blocks_hostname_that_fails_to_resolve():
def fake_resolver(hostname, port):
raise OSError("name resolution failed")
with pytest.raises(ScrapeError) as exc_info:
assert_url_is_safe("http://this-does-not-resolve.invalid/", resolver=fake_resolver)
assert exc_info.value.status_code == 400
def test_blocks_hostname_that_resolves_to_a_private_address_via_injected_resolver():
# Simulates DNS rebinding / an attacker-controlled domain that resolves
# to an internal address, without depending on real DNS in the test.
def fake_resolver(hostname, port):
return [(2, 1, 6, "", ("10.1.2.3", 0))]
with pytest.raises(ScrapeError) as exc_info:
assert_url_is_safe("http://evil.example.com/", resolver=fake_resolver)
assert exc_info.value.status_code == 400
@pytest.mark.parametrize(
"url",
[
"https://example.com/",
"http://example.com/some/path?x=1",
"https://example.com:8443/path",
],
)
def test_allows_public_targets(url):
def fake_resolver(hostname, port):
return [(2, 1, 6, "", ("93.184.216.34", 0))]
assert_url_is_safe(url, resolver=fake_resolver)
def test_missing_hostname_is_rejected():
with pytest.raises(ScrapeError) as exc_info:
assert_url_is_safe("http:///no-host")
assert exc_info.value.status_code == 400