Ctrl K
ComunidadGuías…

tests/test_url_security_resolved.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""SSRF protection: IP normalisation, DNS resolution, redirect guard."""

import asyncio
import socket
from unittest import mock

import pytest

from src.core.url_security import (
    UnsafeURLError,
    assert_safe_resolved,
    is_safe_public_url,
    make_request_guard,
    parse_allowed_hosts,
)


@pytest.mark.parametrize("url", [
    "http://127.1/",
    "http://0x7f000001/",
    "http://2130706433/",
    "http://0177.0.0.1/",
    "http://[::ffff:127.0.0.1]/",
    "http://100.64.0.1/",
    "http://10.1/",
])
def test_numeric_ip_forms_are_rejected(url):
    ok, _ = is_safe_public_url(url)
    assert not ok


@pytest.mark.parametrize("url", [
    "https://example.org/",
    "https://abc.de/",          # hex-looking hostname stays a hostname
    "http://8.8.8.8/",
])
def test_public_targets_pass_lexical_check(url):
    ok, reason = is_safe_public_url(url)
    assert ok, reason


def _fake_getaddrinfo(addresses):
    async def _gai(host, port, type=0):
        return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (a, 0))
                for a in addresses]
    return _gai


def _run(coro):
    return asyncio.run(coro)


def test_hostname_resolving_to_internal_address_is_rejected():
    async def go():
        loop = asyncio.get_running_loop()
        with mock.patch.object(loop, "getaddrinfo",
                               _fake_getaddrinfo(["10.0.0.5"])):
            with pytest.raises(UnsafeURLError):
                await assert_safe_resolved("http://wiki.intern.example/")
    _run(go())


def test_any_internal_address_among_several_is_rejected():
    async def go():
        loop = asyncio.get_running_loop()
        with mock.patch.object(loop, "getaddrinfo",
                               _fake_getaddrinfo(["93.184.216.34", "127.0.0.1"])):
            with pytest.raises(UnsafeURLError):
                await assert_safe_resolved("http://mixed.example/")
    _run(go())


def test_public_resolution_passes():
    async def go():
        loop = asyncio.get_running_loop()
        with mock.patch.object(loop, "getaddrinfo",
                               _fake_getaddrinfo(["93.184.216.34"])):
            await assert_safe_resolved("http://public.example/")
    _run(go())


def test_allow_list_skips_resolution():
    async def go():
        loop = asyncio.get_running_loop()
        gai = mock.AsyncMock(side_effect=AssertionError("must not resolve"))
        with mock.patch.object(loop, "getaddrinfo", gai):
            await assert_safe_resolved(
                "http://wiki.intern.example/",
                parse_allowed_hosts(" Wiki.Intern.Example , other "),
            )
    _run(go())


def test_request_guard_checks_each_request():
    class _Req:
        def __init__(self, url):
            self.url = url

    async def go():
        guard = make_request_guard()
        with pytest.raises(UnsafeURLError):
            await guard(_Req("http://127.0.0.1/after-redirect"))
    _run(go())