461bf6fd40
CI / lint (3.11) (push) Blocked by required conditions
CI / lint (3.12) (push) Blocked by required conditions
CI / lint (3.13) (push) Blocked by required conditions
CI / shellcheck (push) Waiting to run
CI / shfmt (push) Waiting to run
CI / setup (3.11) (push) Waiting to run
CI / setup (3.12) (push) Waiting to run
CI / setup (3.13) (push) Waiting to run
CI / check-licenses (3.12) (push) Blocked by required conditions
CI / test_unit (3.11) (push) Blocked by required conditions
CI / test_unit (3.12) (push) Blocked by required conditions
CI / test_unit (3.13) (push) Blocked by required conditions
CI / test_unit_no_extras (3.11) (push) Blocked by required conditions
CI / test_unit_no_extras (3.12) (push) Blocked by required conditions
CI / test_json_to_html (3.12) (push) Blocked by required conditions
CI / test_unit_no_extras (3.13) (push) Blocked by required conditions
CI / test_unit_dependency_extras (csv, 3.11, --extra csv) (push) Blocked by required conditions
CI / test_unit_dependency_extras (csv, 3.12, --extra csv) (push) Blocked by required conditions
CI / test_unit_dependency_extras (csv, 3.13, --extra csv) (push) Blocked by required conditions
CI / test_unit_dependency_extras (docx, 3.11, --extra docx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (docx, 3.12, --extra docx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (docx, 3.13, --extra docx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (markdown, 3.11, --extra md) (push) Blocked by required conditions
CI / test_unit_dependency_extras (markdown, 3.12, --extra md) (push) Blocked by required conditions
CI / test_unit_dependency_extras (markdown, 3.13, --extra md) (push) Blocked by required conditions
CI / test_unit_dependency_extras (odt, 3.11, --extra odt) (push) Blocked by required conditions
CI / test_unit_dependency_extras (odt, 3.12, --extra odt) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pdf-image, 3.12, --extra pdf --extra image --extra paddleocr) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pdf-image, 3.13, --extra pdf --extra image --extra paddleocr) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pptx, 3.13, --extra pptx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (odt, 3.13, --extra odt) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pdf-image, 3.11, --extra pdf --extra image --extra paddleocr) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pptx, 3.11, --extra pptx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pptx, 3.12, --extra pptx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pypandoc, 3.11, --extra epub --extra org --extra rtf --extra rst) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pypandoc, 3.12, --extra epub --extra org --extra rtf --extra rst) (push) Blocked by required conditions
CI / test_unit_dependency_extras (pypandoc, 3.13, --extra epub --extra org --extra rtf --extra rst) (push) Blocked by required conditions
CI / test_unit_dependency_extras (xlsx, 3.11, --extra xlsx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (xlsx, 3.12, --extra xlsx) (push) Blocked by required conditions
CI / test_unit_dependency_extras (xlsx, 3.13, --extra xlsx) (push) Blocked by required conditions
CI / test_ingest_src (3.12) (push) Blocked by required conditions
CI / test_json_to_markdown (3.12) (push) Blocked by required conditions
CI / changelog (push) Waiting to run
CI / test_dockerfile (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Build And Push Docker Image / set-short-sha (push) Waiting to run
Build And Push Docker Image / build-images (linux/amd64, opensource-linux-8core) (push) Blocked by required conditions
Build And Push Docker Image / build-images (linux/arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Build And Push Docker Image / publish-images (push) Blocked by required conditions
Partition Benchmark / setup (push) Waiting to run
Partition Benchmark / Measure and compare partition() runtime (push) Blocked by required conditions
495 lines
20 KiB
Python
495 lines
20 KiB
Python
"""Tests for centralized URL fetching with host validation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import requests
|
|
|
|
from unstructured.safe_http import (
|
|
UnsafeURLError,
|
|
_is_cross_origin,
|
|
_is_ip_blocked,
|
|
_normalize_hostname,
|
|
_safe_create_connection,
|
|
_SafeHTTPAdapter,
|
|
_strip_sensitive_headers,
|
|
_validate_url,
|
|
safe_get,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _is_ip_blocked
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class Describe_is_ip_blocked:
|
|
"""Verify that blocked and allowed IP ranges are classified correctly."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"ip",
|
|
[
|
|
"127.0.0.1",
|
|
"10.0.0.1",
|
|
"172.16.0.1",
|
|
"192.168.0.1",
|
|
"169.254.169.254",
|
|
"0.0.0.0",
|
|
"255.255.255.255",
|
|
"168.63.129.16",
|
|
"224.0.0.1",
|
|
"240.0.0.1",
|
|
"100.64.0.1",
|
|
],
|
|
ids=[
|
|
"loopback",
|
|
"class_a_private",
|
|
"class_b_private",
|
|
"class_c_private",
|
|
"link_local",
|
|
"this_network",
|
|
"broadcast",
|
|
"azure_wireserver",
|
|
"multicast",
|
|
"reserved_class_e",
|
|
"cgnat",
|
|
],
|
|
)
|
|
def test_blocks_non_routable_ipv4(self, ip: str):
|
|
assert _is_ip_blocked(ip) is True
|
|
|
|
@pytest.mark.parametrize(
|
|
"ip",
|
|
["::1", "fe80::1", "fc00::1", "fd00::1", "::"],
|
|
ids=["loopback", "link_local", "unique_local_fc", "unique_local_fd", "unspecified"],
|
|
)
|
|
def test_blocks_non_routable_ipv6(self, ip: str):
|
|
assert _is_ip_blocked(ip) is True
|
|
|
|
def test_blocks_ipv4_mapped_ipv6(self):
|
|
assert _is_ip_blocked("::ffff:127.0.0.1") is True
|
|
assert _is_ip_blocked("::ffff:169.254.169.254") is True
|
|
|
|
def test_blocks_sixtofour_embedded(self):
|
|
# 2002:a9fe:a9fe:: wraps 169.254.169.254
|
|
assert _is_ip_blocked("2002:a9fe:a9fe::") is True
|
|
|
|
def test_blocks_nat64_and_ipv4_compat_embedded(self):
|
|
assert _is_ip_blocked("64:ff9b::169.254.169.254") is True
|
|
assert _is_ip_blocked("64:ff9b:1::169.254.169.254") is True
|
|
assert _is_ip_blocked("::169.254.169.254") is True
|
|
|
|
@pytest.mark.parametrize(
|
|
"ip",
|
|
["8.8.8.8", "93.184.216.34", "1.1.1.1", "2607:f8b0:4004:800::200e"],
|
|
ids=["google_dns", "example_com", "cloudflare", "google_ipv6"],
|
|
)
|
|
def test_allows_public_addresses(self, ip: str):
|
|
assert _is_ip_blocked(ip) is False
|
|
|
|
def test_fails_closed_on_unparseable_input(self):
|
|
assert _is_ip_blocked("not-an-ip") is True
|
|
assert _is_ip_blocked("") is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _normalize_hostname
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class Describe_normalize_hostname:
|
|
def test_returns_lowercase_for_ascii_input(self):
|
|
assert _normalize_hostname("Example.COM") == "example.com"
|
|
|
|
def test_strips_trailing_dot(self):
|
|
assert _normalize_hostname("example.com.") == "example.com"
|
|
|
|
def test_empty_string(self):
|
|
assert _normalize_hostname("") == ""
|
|
|
|
def test_idn_collapses_to_punycode(self):
|
|
# A valid IDN encodes to its xn-- form.
|
|
assert _normalize_hostname("Bücher.example").startswith("xn--")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _validate_url
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class Describe_validate_url:
|
|
@pytest.mark.parametrize(
|
|
"url",
|
|
[
|
|
"ftp://example.com/file",
|
|
"file:///etc/passwd",
|
|
"gopher://evil.com/",
|
|
"data:text/html,<h1>hi</h1>",
|
|
"javascript:alert(1)",
|
|
],
|
|
)
|
|
def test_rejects_non_http_scheme(self, url: str):
|
|
with pytest.raises(UnsafeURLError, match="scheme"):
|
|
_validate_url(url)
|
|
|
|
def test_rejects_missing_hostname(self):
|
|
with pytest.raises(UnsafeURLError, match="hostname"):
|
|
_validate_url("http://")
|
|
|
|
@pytest.mark.parametrize(
|
|
"url",
|
|
["http://example.com:65536/", "http://example.com:99999/"],
|
|
ids=["over_max", "way_over"],
|
|
)
|
|
def test_rejects_out_of_range_port(self, url: str):
|
|
# Out-of-range ports must fail closed as UnsafeURLError, not leak later.
|
|
with pytest.raises(UnsafeURLError):
|
|
_validate_url(url)
|
|
|
|
@pytest.mark.parametrize(
|
|
"hostname",
|
|
[
|
|
"localhost",
|
|
"metadata.google.internal",
|
|
"metadata.gke.internal",
|
|
"metadata.azure.com",
|
|
"kubernetes.default",
|
|
"kubernetes.default.svc",
|
|
"kubernetes.default.svc.cluster.local",
|
|
],
|
|
)
|
|
def test_rejects_blocked_hostname(self, hostname: str):
|
|
with pytest.raises(UnsafeURLError, match="blocked hostname"):
|
|
_validate_url(f"http://{hostname}/")
|
|
|
|
@pytest.mark.parametrize(
|
|
"url",
|
|
[
|
|
"http://127.0.0.1/path",
|
|
"http://169.254.169.254/latest/",
|
|
"http://10.0.0.1/internal",
|
|
"http://[::1]/path",
|
|
"http://[::ffff:169.254.169.254]/",
|
|
],
|
|
ids=["loopback", "link_local", "rfc1918", "ipv6_loopback", "ipv4_mapped"],
|
|
)
|
|
def test_rejects_blocked_ip_literals(self, url: str):
|
|
with pytest.raises(UnsafeURLError, match="blocked"):
|
|
_validate_url(url)
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
def test_rejects_hostname_resolving_to_non_routable(self, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("10.0.0.1", 0))]
|
|
with pytest.raises(UnsafeURLError, match="blocked address"):
|
|
_validate_url("http://evil.example.com/")
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
def test_allows_public_hostname(self, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
|
|
_validate_url("https://example.com/doc.pdf") # should not raise
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
def test_rejects_when_any_resolved_ip_is_blocked(self, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [
|
|
(2, 1, 6, "", ("93.184.216.34", 0)),
|
|
(2, 1, 6, "", ("10.0.0.1", 0)),
|
|
]
|
|
with pytest.raises(UnsafeURLError, match="blocked address"):
|
|
_validate_url("http://split.example.com/")
|
|
|
|
def test_skips_validation_with_allow_private(self):
|
|
_validate_url("http://127.0.0.1/", allow_private=True) # should not raise
|
|
|
|
def test_skips_validation_with_env_var(self, monkeypatch):
|
|
monkeypatch.setenv("UNSTRUCTURED_ALLOW_PRIVATE_URL", "1")
|
|
_validate_url("http://127.0.0.1/") # should not raise
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _safe_create_connection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class Describe_safe_create_connection:
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
def test_rejects_blocked_resolved_address_at_connect(self, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("10.0.0.1", 0))]
|
|
with pytest.raises(UnsafeURLError, match="blocked address"):
|
|
_safe_create_connection("evil.example.com", 80, None, None, None)
|
|
|
|
@patch("unstructured.safe_http.socket.socket")
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
def test_connects_to_validated_public_address(self, mock_getaddrinfo, MockSocket):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 80))]
|
|
sock = MockSocket.return_value
|
|
result = _safe_create_connection("example.com", 80, None, None, None)
|
|
assert result is sock
|
|
sock.connect.assert_called_once_with(("93.184.216.34", 80))
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
def test_strips_ipv6_brackets(self, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(10, 1, 6, "", ("::1", 80, 0, 0))]
|
|
with pytest.raises(UnsafeURLError):
|
|
_safe_create_connection("[::1]", 80, None, None, None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# safe_get
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class Describe_safe_get:
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_returns_response_for_public_url(self, MockSession, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
|
|
mock_response = MagicMock(spec=requests.Response)
|
|
mock_response.is_redirect = False
|
|
session_instance = MockSession.return_value
|
|
session_instance.get.return_value = mock_response
|
|
|
|
result = safe_get("https://example.com/doc.pdf")
|
|
|
|
assert result is mock_response
|
|
session_instance.get.assert_called_once()
|
|
session_instance.close.assert_called_once()
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_applies_default_timeout(self, MockSession, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
|
|
mock_response = MagicMock(spec=requests.Response)
|
|
mock_response.is_redirect = False
|
|
MockSession.return_value.get.return_value = mock_response
|
|
|
|
safe_get("https://example.com/doc.pdf")
|
|
|
|
assert MockSession.return_value.get.call_args[1]["timeout"] == (10, 300)
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_respects_explicit_timeout(self, MockSession, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
|
|
mock_response = MagicMock(spec=requests.Response)
|
|
mock_response.is_redirect = False
|
|
MockSession.return_value.get.return_value = mock_response
|
|
|
|
safe_get("https://example.com/doc.pdf", timeout=5)
|
|
|
|
assert MockSession.return_value.get.call_args[1]["timeout"] == 5
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_forces_allow_redirects_false(self, MockSession, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
|
|
mock_response = MagicMock(spec=requests.Response)
|
|
mock_response.is_redirect = False
|
|
MockSession.return_value.get.return_value = mock_response
|
|
|
|
safe_get("https://example.com/doc.pdf", allow_redirects=True)
|
|
|
|
assert MockSession.return_value.get.call_args[1]["allow_redirects"] is False
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_validates_redirect_targets(self, MockSession, mock_getaddrinfo):
|
|
mock_getaddrinfo.side_effect = [
|
|
[(2, 1, 6, "", ("93.184.216.34", 0))], # initial URL
|
|
[(2, 1, 6, "", ("10.0.0.1", 0))], # redirect target
|
|
]
|
|
redirect_response = MagicMock(spec=requests.Response)
|
|
redirect_response.is_redirect = True
|
|
redirect_response.headers = {"Location": "http://internal.corp/secret"}
|
|
redirect_response.url = "https://example.com/doc.pdf"
|
|
MockSession.return_value.get.return_value = redirect_response
|
|
|
|
with pytest.raises(UnsafeURLError, match="blocked address"):
|
|
safe_get("https://example.com/doc.pdf")
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_follows_valid_redirect_chain(self, MockSession, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
|
|
redirect_response = MagicMock(spec=requests.Response)
|
|
redirect_response.is_redirect = True
|
|
redirect_response.headers = {"Location": "https://cdn.example.com/doc.pdf"}
|
|
redirect_response.url = "https://example.com/doc.pdf"
|
|
final_response = MagicMock(spec=requests.Response)
|
|
final_response.is_redirect = False
|
|
MockSession.return_value.get.side_effect = [redirect_response, final_response]
|
|
|
|
result = safe_get("https://example.com/doc.pdf")
|
|
|
|
assert result is final_response
|
|
assert MockSession.return_value.get.call_count == 2
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_enforces_max_redirects(self, MockSession, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
|
|
redirect_response = MagicMock(spec=requests.Response)
|
|
redirect_response.is_redirect = True
|
|
redirect_response.headers = {"Location": "https://example.com/again"}
|
|
redirect_response.url = "https://example.com/doc.pdf"
|
|
MockSession.return_value.get.return_value = redirect_response
|
|
|
|
with pytest.raises(UnsafeURLError, match="Too many redirects"):
|
|
safe_get("https://example.com/doc.pdf")
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_returns_response_on_missing_location_header(self, MockSession, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
|
|
redirect_response = MagicMock(spec=requests.Response)
|
|
redirect_response.is_redirect = True
|
|
redirect_response.headers = {}
|
|
redirect_response.url = "https://example.com/doc.pdf"
|
|
MockSession.return_value.get.return_value = redirect_response
|
|
|
|
result = safe_get("https://example.com/doc.pdf")
|
|
assert result is redirect_response
|
|
|
|
def test_rejects_non_routable_url_without_fetch(self):
|
|
with pytest.raises(UnsafeURLError):
|
|
safe_get("http://169.254.169.254/latest/meta-data/")
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_session_closed_on_validation_error(self, MockSession, mock_getaddrinfo):
|
|
mock_getaddrinfo.side_effect = [
|
|
[(2, 1, 6, "", ("93.184.216.34", 0))],
|
|
[(2, 1, 6, "", ("10.0.0.1", 0))],
|
|
]
|
|
redirect_response = MagicMock(spec=requests.Response)
|
|
redirect_response.is_redirect = True
|
|
redirect_response.headers = {"Location": "http://internal.corp/"}
|
|
redirect_response.url = "https://example.com/doc.pdf"
|
|
MockSession.return_value.get.return_value = redirect_response
|
|
|
|
with pytest.raises(UnsafeURLError):
|
|
safe_get("https://example.com/doc.pdf")
|
|
MockSession.return_value.close.assert_called_once()
|
|
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
def test_rejects_proxies_kwarg(self, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
|
|
with pytest.raises(UnsafeURLError, match="proxies"):
|
|
safe_get("https://example.com/doc.pdf", proxies={"https": "http://proxy:8080"})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# safe_get adapter mounting / opt-out
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class Describe_safe_get_mounts_adapter:
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_mounts_safe_adapter_and_disables_trust_env(self, MockSession, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
|
|
mock_response = MagicMock(spec=requests.Response)
|
|
mock_response.is_redirect = False
|
|
session_instance = MockSession.return_value
|
|
session_instance.get.return_value = mock_response
|
|
|
|
safe_get("https://example.com/doc.pdf")
|
|
|
|
assert session_instance.trust_env is False
|
|
mounted = [c.args[1] for c in session_instance.mount.call_args_list]
|
|
assert all(isinstance(a, _SafeHTTPAdapter) for a in mounted)
|
|
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_allow_private_skips_adapter_and_permits_proxies(self, MockSession):
|
|
mock_response = MagicMock(spec=requests.Response)
|
|
mock_response.is_redirect = False
|
|
session_instance = MockSession.return_value
|
|
session_instance.get.return_value = mock_response
|
|
|
|
# No getaddrinfo patch: allow_private must skip resolution entirely.
|
|
result = safe_get(
|
|
"http://127.0.0.1/internal",
|
|
allow_private=True,
|
|
proxies={"http": "http://proxy:8080"},
|
|
)
|
|
assert result is mock_response
|
|
session_instance.mount.assert_not_called()
|
|
|
|
|
|
class Describe_SafeHTTPAdapter:
|
|
def test_proxy_manager_is_refused(self):
|
|
adapter = _SafeHTTPAdapter()
|
|
with pytest.raises(UnsafeURLError, match="Proxied requests"):
|
|
adapter.proxy_manager_for("http://proxy:8080")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _is_cross_origin / _strip_sensitive_headers (redirect credential handling)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class Describe_is_cross_origin:
|
|
def test_same_origin_not_cross(self):
|
|
assert _is_cross_origin("https://example.com/a", "https://example.com/b") is False
|
|
|
|
def test_different_host_is_cross(self):
|
|
assert _is_cross_origin("https://a.com/", "https://b.com/") is True
|
|
|
|
def test_http_to_https_upgrade_is_not_cross(self):
|
|
assert _is_cross_origin("http://example.com/", "https://example.com/") is False
|
|
|
|
def test_downgrade_on_same_nondefault_port_is_cross(self):
|
|
assert _is_cross_origin("https://host:9000/a", "http://host:9000/b") is True
|
|
|
|
def test_explicit_default_port_same_origin_not_cross(self):
|
|
assert _is_cross_origin("https://example.com:443/a", "https://example.com/b") is False
|
|
|
|
def test_unparseable_port_fails_safe(self):
|
|
assert _is_cross_origin("http://a.com:abc/", "http://a.com/") is True
|
|
|
|
|
|
class Describe_strip_sensitive_headers:
|
|
def test_removes_credential_headers(self):
|
|
kwargs = {
|
|
"headers": {
|
|
"Authorization": "Bearer x",
|
|
"Cookie": "s=y",
|
|
"Proxy-Authorization": "Basic z",
|
|
"User-Agent": "tests",
|
|
}
|
|
}
|
|
_strip_sensitive_headers(kwargs)
|
|
assert kwargs["headers"] == {"User-Agent": "tests"}
|
|
|
|
def test_removes_auth_and_cookies_kwargs(self):
|
|
kwargs = {"auth": ("u", "p"), "cookies": {"s": "y"}, "timeout": 5}
|
|
_strip_sensitive_headers(kwargs)
|
|
assert "auth" not in kwargs
|
|
assert "cookies" not in kwargs
|
|
assert kwargs["timeout"] == 5
|
|
|
|
def test_no_headers_is_noop(self):
|
|
kwargs: dict = {}
|
|
_strip_sensitive_headers(kwargs)
|
|
assert kwargs == {}
|
|
|
|
|
|
class Describe_safe_get_redirect_auth_strip:
|
|
@patch("unstructured.safe_http.socket.getaddrinfo")
|
|
@patch("unstructured.safe_http.requests.Session")
|
|
def test_strips_credentials_on_cross_origin_redirect(self, MockSession, mock_getaddrinfo):
|
|
mock_getaddrinfo.return_value = [(2, 1, 6, "", ("93.184.216.34", 0))]
|
|
redirect_response = MagicMock(spec=requests.Response)
|
|
redirect_response.is_redirect = True
|
|
redirect_response.headers = {"Location": "https://evil.com/x"}
|
|
redirect_response.url = "https://trusted.com/doc"
|
|
final_response = MagicMock(spec=requests.Response)
|
|
final_response.is_redirect = False
|
|
session_instance = MockSession.return_value
|
|
session_instance.get.side_effect = [redirect_response, final_response]
|
|
|
|
safe_get("https://trusted.com/doc", headers={"Authorization": "Bearer secret"})
|
|
|
|
second_call_headers = session_instance.get.call_args_list[1][1].get("headers", {})
|
|
assert "Authorization" not in second_call_headers
|