From afb8b367ee537c58853c9b2c9a674728969f79eb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 02:58:26 +0000 Subject: [PATCH 1/5] Fix SyntaxError caused by duplicate `event_hooks` in `httpx.AsyncClient` instantiation Combined duplicated `event_hooks` keyword arguments into a single dictionary parameter with both `validate_redirect` and `verify_redirect` in `app/api/url_upload.py`. This fixes a `SyntaxError: keyword argument repeated: event_hooks` and ensures that all redirect validations run. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/api/url_upload.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/api/url_upload.py b/app/api/url_upload.py index 1df8d020..9f20039c 100644 --- a/app/api/url_upload.py +++ b/app/api/url_upload.py @@ -194,11 +194,10 @@ async def process_url( async with httpx.AsyncClient( timeout=settings.http_request_timeout, follow_redirects=True, - event_hooks={"response": [validate_redirect]}, + event_hooks={"response": [validate_redirect, verify_redirect]}, headers={ "User-Agent": "DocuElevate/1.0", # Identify ourselves }, - event_hooks={"response": [verify_redirect]}, ) as client: async with client.stream("GET", url) as response: response.raise_for_status() From 18f5596b0149ffe44441e39f6d315fca16fe963a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 03:01:11 +0000 Subject: [PATCH 2/5] Fix SyntaxError caused by duplicate `event_hooks` in `httpx.AsyncClient` instantiation Combined duplicated `event_hooks` keyword arguments into a single dictionary parameter with both `validate_redirect` and `verify_redirect` in `app/api/url_upload.py`. This fixes a `SyntaxError: keyword argument repeated: event_hooks` and ensures that all redirect validations run. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> From 871f788f0bd782ba8ad3a7d70e5cd4ccd24f749b Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Sun, 17 May 2026 13:12:55 +0200 Subject: [PATCH 3/5] fix(url-upload): handle unsafe redirects as client errors --- app/api/url_upload.py | 30 +++++++++++++----------------- tests/test_url_upload.py | 13 +++++++++++++ 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/app/api/url_upload.py b/app/api/url_upload.py index 9f20039c..b255407f 100644 --- a/app/api/url_upload.py +++ b/app/api/url_upload.py @@ -28,6 +28,10 @@ logger = logging.getLogger(__name__) router = APIRouter() +class UnsafeRedirectError(httpx.RequestError): + """Raised when a redirect target fails URL safety checks.""" + + class URLUploadRequest(BaseModel): """Request model for URL-based file upload""" @@ -120,9 +124,10 @@ async def verify_redirect(response: httpx.Response) -> None: try: validate_url_safety(new_url) except HTTPException as e: - # Map the validation error to an httpx exception so it can be handled - # properly by the caller, avoiding raw HTTPExceptions escaping the client scope - raise httpx.RequestError(f"Redirect to unsafe URL blocked: {e.detail}", request=response.request) from e + raise UnsafeRedirectError( + f"Redirect to unsafe URL blocked: {e.detail}", + request=response.request, + ) from e @router.post("/process-url") @@ -170,19 +175,6 @@ async def process_url( if not safe_filename: safe_filename = "download" - # Hook to validate redirects and prevent SSRF - async def validate_redirect(response: httpx.Response): - if response.is_redirect: - location = response.headers.get("Location") - if location: - # Resolve relative URLs - next_url = urllib.parse.urljoin(str(response.url), location) - try: - validate_url_safety(next_url) - except HTTPException as e: - # Reraise as a RequestError so httpx aborts the request - raise httpx.RequestError(f"Unsafe redirect target: {e.detail}", request=response.request) - # Download file with security measures # Initialize target_path to None to prevent UnboundLocalError in exception handlers # that may execute before target_path is assigned during error cases @@ -194,7 +186,7 @@ async def process_url( async with httpx.AsyncClient( timeout=settings.http_request_timeout, follow_redirects=True, - event_hooks={"response": [validate_redirect, verify_redirect]}, + event_hooks={"response": [verify_redirect]}, headers={ "User-Agent": "DocuElevate/1.0", # Identify ourselves }, @@ -284,6 +276,10 @@ async def process_url( logger.error(f"HTTP error while downloading file from URL: {url} - {str(e)}") raise HTTPException(status_code=e.response.status_code, detail=f"HTTP error: {str(e)}") + except UnsafeRedirectError as e: + logger.warning(f"Unsafe redirect blocked while downloading file from URL: {url} - {str(e)}") + raise HTTPException(status_code=400, detail=str(e)) + except httpx.RequestError as e: logger.error(f"Error downloading file from URL: {url} - {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to download file: {str(e)}") diff --git a/tests/test_url_upload.py b/tests/test_url_upload.py index b3caaff8..0c83e440 100644 --- a/tests/test_url_upload.py +++ b/tests/test_url_upload.py @@ -465,6 +465,19 @@ class TestURLUploadEndpoint: data = response.json() assert "Failed to download file" in data["detail"] + @patch("app.api.url_upload.httpx.AsyncClient.stream") + def test_process_url_unsafe_redirect_returns_400(self, mock_stream, client): + """Test unsafe redirects are reported as a client error instead of HTTP 500.""" + from app.api.url_upload import UnsafeRedirectError + + mock_stream.side_effect = UnsafeRedirectError("Redirect to unsafe URL blocked: Unsafe URL") + + response = client.post("/api/process-url", json={"url": "https://example.com/file.pdf"}) + + assert response.status_code == 400 + data = response.json() + assert "Redirect to unsafe URL blocked" in data["detail"] + @patch("app.api.url_upload.httpx.AsyncClient.stream") def test_process_url_oserror_during_save(self, mock_stream, client, tmp_path, monkeypatch): """Test handling of OSError when saving file""" From 044a9a86d6dbf5e90fcfc7f3e046fca58740332e Mon Sep 17 00:00:00 2001 From: semantic-release Date: Sun, 17 May 2026 12:40:20 +0000 Subject: [PATCH 4/5] 0.172.10 Automatically generated by python-semantic-release --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf9d996..a826faeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## v0.172.10 (2026-05-17) + +### Bug Fixes + +- **url-upload**: Handle unsafe redirects as client errors + ([`871f788`](https://github.com/christianlouis/DocuElevate/commit/871f788f0bd782ba8ad3a7d70e5cd4ccd24f749b)) + +### Documentation + +- **changelog**: Update changelog [skip ci] + ([`58b14ae`](https://github.com/christianlouis/DocuElevate/commit/58b14ae769b85e25290126256de936743609af06)) + + ## Unreleased From 06507ed8bf3ddf5d1bac1079513564515f34188d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 17 May 2026 12:40:24 +0000 Subject: [PATCH 5/5] chore(release): update build metadata files [skip ci] --- BUILD_DATE | 2 +- GIT_SHA | 2 +- RUNTIME_INFO | 12 ++++++------ VERSION | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILD_DATE b/BUILD_DATE index 4fdb54a9..4d6a80ba 100644 --- a/BUILD_DATE +++ b/BUILD_DATE @@ -1 +1 @@ -2026-04-07T09:34:53Z +2026-05-17T12:40:20Z diff --git a/GIT_SHA b/GIT_SHA index 0a799c3b..93306099 100644 --- a/GIT_SHA +++ b/GIT_SHA @@ -1 +1 @@ -3bd8a52 +62d4ca6 diff --git a/RUNTIME_INFO b/RUNTIME_INFO index 88dbc05a..13423581 100644 --- a/RUNTIME_INFO +++ b/RUNTIME_INFO @@ -1,10 +1,10 @@ DocuElevate Build Information ============================== -Version: 0.172.9 -Build Date: 2026-04-07T09:34:53Z -Git Commit: 3bd8a52ea201b33d6071c9b3a7fdace582e65fd5 -Git Short SHA: 3bd8a52 +Version: 0.172.10 +Build Date: 2026-05-17T12:40:20Z +Git Commit: 62d4ca6367a6c8a2e7909305fec56c5b2c24e312 +Git Short SHA: 62d4ca6 Git Branch: main -Commit Date: 2026-04-07T11:34:28+02:00 -Build Timestamp: 2026-04-07T09:34:53Z +Commit Date: 2026-05-17T14:39:59+02:00 +Build Timestamp: 2026-05-17T12:40:20Z ============================== diff --git a/VERSION b/VERSION index e80037f8..ab68e419 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.172.9 +0.172.10