Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

787 lines
28 KiB
Python

"""Tests for safe_requests module - SSRF-protected HTTP requests."""
import pytest
from unittest.mock import patch, MagicMock
import requests
from local_deep_research.security import ssrf_validator
from local_deep_research.security.safe_requests import (
safe_get,
safe_post,
SafeSession,
DEFAULT_TIMEOUT,
MAX_RESPONSE_SIZE,
)
class TestSafeGetFunction:
"""Tests for safe_get function."""
def test_valid_url_makes_request(self):
"""Should make request to valid external URL."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.get"
) as mock_get:
mock_response = MagicMock()
mock_response.headers = {}
mock_get.return_value = mock_response
response = safe_get("https://example.com")
mock_get.assert_called_once()
assert response == mock_response
def test_rejects_invalid_url(self):
"""Should raise ValueError for URLs failing SSRF validation."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=False,
):
with pytest.raises(ValueError, match="SSRF"):
safe_get("http://127.0.0.1/admin")
def test_uses_default_timeout(self):
"""Should use default timeout when not specified."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.get"
) as mock_get:
mock_response = MagicMock()
mock_response.headers = {}
mock_get.return_value = mock_response
safe_get("https://example.com")
_, kwargs = mock_get.call_args
assert kwargs["timeout"] == DEFAULT_TIMEOUT
def test_custom_timeout(self):
"""Should use custom timeout when provided."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.get"
) as mock_get:
mock_response = MagicMock()
mock_response.headers = {}
mock_get.return_value = mock_response
safe_get("https://example.com", timeout=60)
_, kwargs = mock_get.call_args
assert kwargs["timeout"] == 60
def test_underlying_requests_always_gets_redirects_disabled(self):
"""The underlying requests.get() always receives allow_redirects=False;
safe_get handles redirects manually with SSRF validation."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.get"
) as mock_get:
mock_response = MagicMock()
mock_response.headers = {}
mock_get.return_value = mock_response
safe_get("https://example.com")
_, kwargs = mock_get.call_args
assert kwargs["allow_redirects"] is False
def test_can_enable_redirects(self):
"""With allow_redirects=True, redirects are still disabled at
requests level (handled manually for SSRF validation)."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.get"
) as mock_get:
mock_response = MagicMock()
mock_response.headers = {}
mock_get.return_value = mock_response
safe_get("https://example.com", allow_redirects=True)
_, kwargs = mock_get.call_args
# Redirects are always disabled at the requests level;
# safe_get handles them manually with SSRF validation
assert kwargs["allow_redirects"] is False
def test_passes_params(self):
"""Should pass URL parameters."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.get"
) as mock_get:
mock_response = MagicMock()
mock_response.headers = {}
mock_get.return_value = mock_response
params = {"q": "test", "page": "1"}
safe_get("https://example.com", params=params)
args, _ = mock_get.call_args
assert args == ("https://example.com",)
_, kwargs = mock_get.call_args
assert "params" not in kwargs or kwargs.get("params") == params
def test_oversized_response_raises_error(self):
"""Oversized responses are rejected with ValueError."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.get"
) as mock_get:
mock_response = MagicMock()
mock_response.headers = {
"Content-Length": str(MAX_RESPONSE_SIZE + 1)
}
mock_get.return_value = mock_response
with pytest.raises(ValueError, match="Response too large"):
safe_get("https://example.com")
def test_accepts_response_within_limit(self):
"""Should accept response within size limit."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.get"
) as mock_get:
mock_response = MagicMock()
mock_response.headers = {"Content-Length": str(1024)}
mock_get.return_value = mock_response
response = safe_get("https://example.com")
assert response == mock_response
def test_handles_invalid_content_length(self):
"""Should ignore invalid Content-Length values."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.get"
) as mock_get:
mock_response = MagicMock()
mock_response.headers = {"Content-Length": "not-a-number"}
mock_get.return_value = mock_response
# Should not raise
response = safe_get("https://example.com")
assert response == mock_response
def test_reraises_timeout(self):
"""Should re-raise timeout exceptions."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.get",
side_effect=requests.Timeout("timeout"),
):
with pytest.raises(requests.Timeout):
safe_get("https://example.com")
def test_reraises_request_exception(self):
"""Should re-raise request exceptions."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.get",
side_effect=requests.RequestException("connection error"),
):
with pytest.raises(requests.RequestException):
safe_get("https://example.com")
def test_allow_localhost_parameter(self):
"""Should pass allow_localhost to validate_url."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
) as mock_validate:
with patch(
"local_deep_research.security.safe_requests.requests.get"
) as mock_get:
mock_response = MagicMock()
mock_response.headers = {}
mock_get.return_value = mock_response
safe_get("http://localhost:8080", allow_localhost=True)
mock_validate.assert_called_once_with(
"http://localhost:8080",
allow_localhost=True,
allow_private_ips=False,
)
def test_allow_private_ips_parameter(self):
"""Should pass allow_private_ips to validate_url."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
) as mock_validate:
with patch(
"local_deep_research.security.safe_requests.requests.get"
) as mock_get:
mock_response = MagicMock()
mock_response.headers = {}
mock_get.return_value = mock_response
safe_get("http://192.168.1.1", allow_private_ips=True)
mock_validate.assert_called_once_with(
"http://192.168.1.1",
allow_localhost=False,
allow_private_ips=True,
)
class TestSafePostFunction:
"""Tests for safe_post function."""
def test_valid_url_makes_request(self):
"""Should make POST request to valid external URL."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.post"
) as mock_post:
mock_response = MagicMock()
mock_response.headers = {}
mock_post.return_value = mock_response
response = safe_post(
"https://example.com/api", json={"key": "value"}
)
mock_post.assert_called_once()
assert response == mock_response
def test_rejects_invalid_url(self):
"""Should raise ValueError for URLs failing SSRF validation."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=False,
):
with pytest.raises(ValueError, match="SSRF"):
safe_post("http://169.254.169.254/metadata")
def test_passes_data_parameter(self):
"""Should pass data parameter for form data."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.post"
) as mock_post:
mock_response = MagicMock()
mock_response.headers = {}
mock_post.return_value = mock_response
safe_post("https://example.com", data="raw data")
_, kwargs = mock_post.call_args
assert kwargs.get("data") == "raw data"
def test_passes_json_parameter(self):
"""Should pass json parameter for JSON data."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.post"
) as mock_post:
mock_response = MagicMock()
mock_response.headers = {}
mock_post.return_value = mock_response
json_data = {"key": "value"}
safe_post("https://example.com", json=json_data)
_, kwargs = mock_post.call_args
assert kwargs.get("json") == json_data
def test_uses_default_timeout(self):
"""Should use default timeout when not specified."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.post"
) as mock_post:
mock_response = MagicMock()
mock_response.headers = {}
mock_post.return_value = mock_response
safe_post("https://example.com")
_, kwargs = mock_post.call_args
assert kwargs["timeout"] == DEFAULT_TIMEOUT
def test_underlying_requests_always_gets_redirects_disabled(self):
"""The underlying requests.post() always receives allow_redirects=False;
safe_post handles redirects manually with SSRF validation."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.post"
) as mock_post:
mock_response = MagicMock()
mock_response.headers = {}
mock_post.return_value = mock_response
safe_post("https://example.com")
_, kwargs = mock_post.call_args
assert kwargs["allow_redirects"] is False
def test_can_enable_redirects(self):
"""With allow_redirects=True, redirects are still disabled at
requests level (handled manually for SSRF validation)."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.post"
) as mock_post:
mock_response = MagicMock()
mock_response.headers = {}
mock_post.return_value = mock_response
safe_post("https://example.com", allow_redirects=True)
_, kwargs = mock_post.call_args
# Redirects are always disabled at the requests level;
# safe_post handles them manually with SSRF validation
assert kwargs["allow_redirects"] is False
def test_oversized_response_raises_error(self):
"""Oversized responses are rejected with ValueError."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.post"
) as mock_post:
mock_response = MagicMock()
mock_response.headers = {
"Content-Length": str(MAX_RESPONSE_SIZE + 1)
}
mock_post.return_value = mock_response
with pytest.raises(ValueError, match="Response too large"):
safe_post("https://example.com")
def test_reraises_timeout(self):
"""Should re-raise timeout exceptions."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch(
"local_deep_research.security.safe_requests.requests.post",
side_effect=requests.Timeout("timeout"),
):
with pytest.raises(requests.Timeout):
safe_post("https://example.com")
class TestSafeSession:
"""Tests for SafeSession class."""
def test_init_default_values(self):
"""Should initialize with default security settings."""
session = SafeSession()
assert session.allow_localhost is False
assert session.allow_private_ips is False
def test_init_allow_localhost(self):
"""Should accept allow_localhost parameter."""
session = SafeSession(allow_localhost=True)
assert session.allow_localhost is True
assert session.allow_private_ips is False
def test_init_allow_private_ips(self):
"""Should accept allow_private_ips parameter."""
session = SafeSession(allow_private_ips=True)
assert session.allow_localhost is False
assert session.allow_private_ips is True
def test_request_validates_url(self):
"""Should validate URL before making request."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=False,
):
session = SafeSession()
with pytest.raises(ValueError, match="SSRF"):
session.request("GET", "http://127.0.0.1/admin")
def test_request_makes_call_on_valid_url(self):
"""Should make request when URL is valid."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch.object(requests.Session, "request") as mock_request:
mock_response = MagicMock()
mock_request.return_value = mock_response
session = SafeSession()
response = session.request("GET", "https://example.com")
mock_request.assert_called_once()
assert response == mock_response
def test_request_uses_default_timeout(self):
"""Should set default timeout if not provided."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch.object(requests.Session, "request") as mock_request:
mock_response = MagicMock()
mock_request.return_value = mock_response
session = SafeSession()
session.request("GET", "https://example.com")
_, kwargs = mock_request.call_args
assert kwargs["timeout"] == DEFAULT_TIMEOUT
def test_request_respects_custom_timeout(self):
"""Should respect custom timeout when provided."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with patch.object(requests.Session, "request") as mock_request:
mock_response = MagicMock()
mock_request.return_value = mock_response
session = SafeSession()
session.request("GET", "https://example.com", timeout=120)
_, kwargs = mock_request.call_args
assert kwargs["timeout"] == 120
def test_context_manager(self):
"""Should work as context manager."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
):
with SafeSession() as session:
assert isinstance(session, SafeSession)
def test_passes_allow_localhost_to_validate(self):
"""Should pass allow_localhost to validate_url."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
) as mock_validate:
with patch.object(requests.Session, "request"):
session = SafeSession(allow_localhost=True)
session.request("GET", "http://localhost:8080")
mock_validate.assert_called_once_with(
"http://localhost:8080",
allow_localhost=True,
allow_private_ips=False,
)
def test_passes_allow_private_ips_to_validate(self):
"""Should pass allow_private_ips to validate_url."""
with patch.object(
ssrf_validator,
"validate_url",
return_value=True,
) as mock_validate:
with patch.object(requests.Session, "request"):
session = SafeSession(allow_private_ips=True)
session.request("GET", "http://192.168.1.1")
mock_validate.assert_called_once_with(
"http://192.168.1.1",
allow_localhost=False,
allow_private_ips=True,
)
class TestConstants:
"""Tests for module constants."""
def test_default_timeout_reasonable(self):
"""DEFAULT_TIMEOUT should be a reasonable value."""
assert DEFAULT_TIMEOUT == 30
assert isinstance(DEFAULT_TIMEOUT, int)
def test_max_response_size_reasonable(self):
"""MAX_RESPONSE_SIZE should be a reasonable value (1GB)."""
assert MAX_RESPONSE_SIZE == 1024 * 1024 * 1024 # 1GB
assert isinstance(MAX_RESPONSE_SIZE, int)
class TestUserAgentInjection:
"""The project User-Agent should be injected automatically when the
caller doesn't supply one, and preserved when they do.
This locks in the behavior added by PR #3081 — academic API endpoints
(arXiv, OpenAlex, PubMed, …) need to identify the caller, and we
don't want every call site to remember to set the header manually.
"""
def _make_response(self):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.headers = {}
mock_response.url = "https://example.com"
return mock_response
def test_safe_get_injects_user_agent_when_missing(self):
"""safe_get with no headers should auto-inject the project UA."""
from local_deep_research.constants import USER_AGENT
with (
patch.object(ssrf_validator, "validate_url", return_value=True),
patch(
"local_deep_research.security.safe_requests.requests.get",
return_value=self._make_response(),
) as mock_get,
):
safe_get("https://example.com")
_, kwargs = mock_get.call_args
assert "headers" in kwargs
assert kwargs["headers"]["User-Agent"] == USER_AGENT
def test_safe_get_preserves_explicit_user_agent(self):
"""A caller-supplied User-Agent must NOT be overwritten."""
custom_ua = "Mozilla/5.0 Custom/1.0"
with (
patch.object(ssrf_validator, "validate_url", return_value=True),
patch(
"local_deep_research.security.safe_requests.requests.get",
return_value=self._make_response(),
) as mock_get,
):
safe_get("https://example.com", headers={"User-Agent": custom_ua})
_, kwargs = mock_get.call_args
assert kwargs["headers"]["User-Agent"] == custom_ua
def test_safe_get_user_agent_check_is_case_insensitive(self):
"""A `user-agent` (lowercase) header should be respected."""
custom_ua = "Mozilla/5.0 Custom/1.0"
with (
patch.object(ssrf_validator, "validate_url", return_value=True),
patch(
"local_deep_research.security.safe_requests.requests.get",
return_value=self._make_response(),
) as mock_get,
):
safe_get("https://example.com", headers={"user-agent": custom_ua})
_, kwargs = mock_get.call_args
# Original lowercase key preserved; no second User-Agent added
assert kwargs["headers"]["user-agent"] == custom_ua
assert "User-Agent" not in kwargs["headers"]
def test_safe_get_does_not_mutate_caller_headers_dict(self):
"""safe_get must not add User-Agent to the caller's dict."""
caller_headers = {"X-Custom": "value"}
with (
patch.object(ssrf_validator, "validate_url", return_value=True),
patch(
"local_deep_research.security.safe_requests.requests.get",
return_value=self._make_response(),
),
):
safe_get("https://example.com", headers=caller_headers)
# Caller's dict is untouched — User-Agent injection is on a
# copy, not the original.
assert "User-Agent" not in caller_headers
assert caller_headers == {"X-Custom": "value"}
def test_safe_post_injects_user_agent_when_missing(self):
"""safe_post should auto-inject the project UA."""
from local_deep_research.constants import USER_AGENT
with (
patch.object(ssrf_validator, "validate_url", return_value=True),
patch(
"local_deep_research.security.safe_requests.requests.post",
return_value=self._make_response(),
) as mock_post,
):
safe_post("https://example.com", data={"k": "v"})
_, kwargs = mock_post.call_args
assert kwargs["headers"]["User-Agent"] == USER_AGENT
def test_safe_post_preserves_explicit_user_agent(self):
custom_ua = "Mozilla/5.0 Custom/1.0"
with (
patch.object(ssrf_validator, "validate_url", return_value=True),
patch(
"local_deep_research.security.safe_requests.requests.post",
return_value=self._make_response(),
) as mock_post,
):
safe_post(
"https://example.com",
data={"k": "v"},
headers={"User-Agent": custom_ua},
)
_, kwargs = mock_post.call_args
assert kwargs["headers"]["User-Agent"] == custom_ua
class TestParserDifferentialEndToEnd:
"""
End-to-end integration tests for the parser-differential SSRF bypass
fix (GHSA-g23j-2vwm-5c25).
Approach: bind a TCP socket on 127.0.0.1:<random> WITHOUT calling
listen() — the kernel responds RST to any incoming connect. If the
fix regresses and ``safe_get`` actually attempts to connect to the
bound port, ``requests`` raises ``ConnectionError`` (kernel RST), so
a strict ``pytest.raises(ValueError, match=...)`` distinguishes
"validator caught it" from "validator failed and the kernel saved us".
"""
@staticmethod
def _bind_unused_port():
import socket as _socket
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
sock.bind(("127.0.0.1", 0))
return sock
def test_safe_get_blocks_parser_differential_no_socket_connect(self):
from local_deep_research.security.safe_requests import safe_get
sock = self._bind_unused_port()
try:
port = sock.getsockname()[1]
bypass_url = f"http://127.0.0.1:{port}\\@1.1.1.1"
with pytest.raises(ValueError, match="SSRF|security validation"):
safe_get(bypass_url, timeout=2)
finally:
sock.close()
def test_safe_post_blocks_parser_differential(self):
from local_deep_research.security.safe_requests import safe_post
sock = self._bind_unused_port()
try:
port = sock.getsockname()[1]
bypass_url = f"http://127.0.0.1:{port}\\@1.1.1.1"
with pytest.raises(ValueError, match="SSRF|security validation"):
safe_post(bypass_url, data={"k": "v"}, timeout=2)
finally:
sock.close()
def test_safesession_blocks_parser_differential(self):
"""SafeSession validates at both request() and send() — exercises
the double-validation path. This URL contains ``\\`` so Layer 1
catches it at request() before .prepare() canonicalises it."""
from local_deep_research.security.safe_requests import SafeSession
sock = self._bind_unused_port()
try:
port = sock.getsockname()[1]
bypass_url = f"http://127.0.0.1:{port}\\@1.1.1.1"
with SafeSession() as sess:
with pytest.raises(
ValueError, match="SSRF|security validation"
):
sess.get(bypass_url, timeout=2)
finally:
sock.close()
def test_safesession_send_blocks_canonicalised_form(self):
"""
Layer-2 verification: ``SafeSession.send()`` is called with a
``PreparedRequest`` whose URL contains ``%5C`` (the canonicalised
form of ``\\``). Layer 1 doesn't match ``%5C``, so Layer 2's
urllib3-based hostname extraction is what blocks this — proving
Layer 2 carries the load on this path.
"""
from local_deep_research.security.safe_requests import SafeSession
with SafeSession() as sess:
req = requests.Request("GET", "http://127.0.0.1:6666/%5C@1.1.1.1")
prepared = sess.prepare_request(req)
with pytest.raises(ValueError, match="SSRF|security validation"):
sess.send(prepared, timeout=2)