From f026dc1d77eba58eab3ff252551fb12a8af6b5ec Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 14 Apr 2026 11:16:19 -0400 Subject: [PATCH] http: enforce that URL paths are relative This is a defensive guard that prevents callers from passing a full URL that would override self.base_url (as a side effect of urljoin()'s behavior). --- src/vesta/http.py | 10 ++++++++-- tests/test_http.py | 13 +++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/vesta/http.py b/src/vesta/http.py index 9b07cb1..5bb43d2 100644 --- a/src/vesta/http.py +++ b/src/vesta/http.py @@ -32,6 +32,7 @@ from typing import NamedTuple from typing import Optional from urllib.parse import urljoin +from urllib.parse import urlsplit # Re-export as a convenience HTTPError = urllib.error.HTTPError @@ -69,7 +70,7 @@ def __init__( def request( self, method: Literal["GET", "POST"], - url: str, + path: str, *, json: Optional[Any] = None, headers: Optional[Mapping[str, str]] = None, @@ -78,8 +79,13 @@ def request( Raises urllib.error.HTTPError for 4xx/5xx responses. Raises urllib.error.URLError for connection errors. + Raises ValueError if ``path`` is not a relative path. """ - full_url = urljoin(self.base_url, url.lstrip("/")) + parts = urlsplit(path) + if parts.scheme or parts.netloc: + raise ValueError(f"expected a relative path, got {path!r}") + + full_url = urljoin(self.base_url, path.lstrip("/")) request_headers = dict(self.headers) if headers: diff --git a/tests/test_http.py b/tests/test_http.py index 3349137..7db1371 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -90,6 +90,19 @@ def test_json_body(self, mock_response): assert mock["data"] == b'{"key":"value"}' assert mock["headers"]["Content-type"] == "application/json" + @pytest.mark.parametrize( + "path", + [ + "https://evil.example.com/x", + "http://evil.example.com/x", + "//evil.example.com/x", + ], + ) + def test_rejects_non_relative_path(self, path): + client = Client(base_url="https://api.example.com") + with pytest.raises(ValueError, match="expected a relative path"): + client.request("GET", path) + def test_http_error(self, monkeypatch): """Test that urllib.error.HTTPError is raised and propagated correctly.""" client = Client(base_url="https://api.example.com")