diff --git a/src/stac_auth_proxy/handlers/reverse_proxy.py b/src/stac_auth_proxy/handlers/reverse_proxy.py index 40863d8c..f7feb8dd 100644 --- a/src/stac_auth_proxy/handlers/reverse_proxy.py +++ b/src/stac_auth_proxy/handlers/reverse_proxy.py @@ -82,6 +82,15 @@ def _prepare_headers(self, request: Request) -> MutableHeaders: async def proxy_request(self, request: Request) -> Response: """Proxy a request to the upstream STAC API.""" + # An encoded "?" or "#" in the path (e.g. item id "a%3Fb") is decoded by + # the server, so request.url.path is truncated at it ("a") and the + # request would act on a different record than the client addressed. + if "?" in request.scope["path"] or "#" in request.scope["path"]: + return Response( + status_code=400, + content='Path must not contain encoded "?" or "#"', + ) + headers = self._prepare_headers(request) # https://github.com/fastapi/fastapi/discussions/7382#discussioncomment-5136466 diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 1ce9c904..515ed8e2 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -1,5 +1,6 @@ """Test authentication cases for the proxy app.""" +import pytest from fastapi.testclient import TestClient from utils import AppFactory, get_upstream_request @@ -38,3 +39,14 @@ async def test_proxied_headers_with_encoding(source_api_server, mock_upstream): proxied_request = await get_upstream_request(mock_upstream) assert proxied_request.headers.get("accept-encoding") == "gzip" + + +@pytest.mark.parametrize("encoded", ["%3F", "%23"]) +async def test_encoded_path_delimiters_rejected( + source_api_server, mock_upstream, encoded +): + """DELETE of item 'a?b' must not be truncated to, and applied to, item 'a'.""" + client = TestClient(app_factory(upstream_url=source_api_server)) + response = client.delete(f"/collections/c/items/a{encoded}b") + assert response.status_code == 400 + assert mock_upstream.call_count == 0