diff --git a/.jules/sentinel.md b/.jules/sentinel.md index af243c59..0cd6a6b4 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -28,3 +28,7 @@ **Vulnerability:** The `/process-url` endpoint used `httpx.AsyncClient(follow_redirects=True)` after validating the initial user-provided URL against SSRF protections. However, it did not validate the target URLs of any subsequent HTTP redirects, allowing an attacker to provide a safe URL that redirects to an internal/private IP, bypassing the security check. **Learning:** Initial URL validation is insufficient when the HTTP client is configured to follow redirects automatically. The client must be explicitly configured to validate every redirect target. **Prevention:** When using `httpx.AsyncClient(follow_redirects=True)` for user-provided URLs, always implement a redirect validator hook function (e.g., using `event_hooks={'response': [validate_redirect]}`) that resolves the `Location` header and passes it through the same SSRF validation logic before the redirect is followed. +## 2026-03-27 - SyntaxError: keyword argument repeated in httpx event hooks +**Vulnerability:** The `/process-url` endpoint in `app/api/url_upload.py` initialized `httpx.AsyncClient` with the `event_hooks` keyword argument twice. This caused a Python SyntaxError, effectively crashing the API endpoint and preventing any execution. +**Learning:** Python does not allow duplicate keyword arguments. In scenarios where multiple hooks (like local and module-level SSRF interceptors) must be provided to a client, they must be merged into a single list value. +**Prevention:** Combine multiple callables for the same event key into a single list, e.g., `event_hooks={"response": [hook1, hook2]}`. 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()