File size: 2,439 Bytes
ebab0e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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