Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@
**Vulnerability:** The `/radio/proxy/{stream_id}` endpoint in `backend/routers/radio.py` accepted stream URLs from the database (which can be created via user input on `/radio/streams`) and fetched them without validating the URL resolved to a safe external IP. It only checked `url.startswith(("http://", "https://"))`. This created a Server-Side Request Forgery (SSRF) vulnerability.
**Learning:** Checking URL scheme prefixes is insufficient for SSRF protection. If the input allows internal hostnames (e.g., `localhost`) or private IPs, an attacker can proxy requests to internal services through the backend. This is particularly dangerous for proxy endpoints which return the response content.
**Prevention:** Always use centralized SSRF validation (e.g., `validate_safe_url`) to check URLs against a strict list of allowed schemes and block private, loopback, or link-local IP addresses before making any outbound HTTP requests.

## 2025-05-21 - [SSRF via Unvalidated Redirects in proxy_stream]
**Vulnerability:** The `/radio/proxy/{stream_id}` endpoint instantiated an `httpx.AsyncClient` with `follow_redirects=True`. Although the initial user-provided URL was checked against SSRF using `validate_safe_url`, if the server responded with an HTTP redirect (e.g. 302 Found) to an internal URL, the client would follow it without re-validating the target URL, leading to an SSRF vulnerability.
**Learning:** Checking a URL before making a request is insufficient if the HTTP client automatically follows redirects to new URLs. Each redirect target must also be validated against SSRF rules.
**Prevention:** If `follow_redirects=True` must be used, add an `event_hooks={'request': [_validate_request_url]}` hook to `httpx.AsyncClient` to intercept and validate every outbound request URL during the lifecycle of the client, including redirects.
13 changes: 12 additions & 1 deletion backend/routers/radio.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,18 @@ async def proxy_stream(
except ValueError as e:
raise HTTPException(400, f"Invalid or unsafe stream URL: {str(e)}")

client = httpx.AsyncClient(timeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0), follow_redirects=True)
async def _validate_request_url(request: httpx.Request):
try:
validate_safe_url(str(request.url))
except ValueError as e:
# We raise a RequestError here so httpx catches it instead of crashing.
raise httpx.RequestError(f"SSRF validation failed: {e}", request=request)

client = httpx.AsyncClient(
timeout=httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0),
follow_redirects=True,
event_hooks={'request': [_validate_request_url]},
)
try:
req = client.build_request("GET", stream.url)
resp = await client.send(req, stream=True)
Expand Down
Loading