Security: /proxy endpoint is an open SSRF proxy #16

Closed
opened 2026-08-13 19:24:56 +00:00 by paulloth1 · 0 comments
paulloth1 commented 2026-08-13 19:24:56 +00:00 (Migrated from github.com)

Problem

GET /proxy?url=<any-url> (backend/app.py:160-222) fetches whatever URL is passed and returns the response, with no allowlist, no auth, and no restriction on scheme/host. It's reachable unauthenticated on the public HTTPS/HTTP listeners (backend-tls-443.service and backend-http.service both bind 0.0.0.0).

This makes the signage server an open SSRF proxy:

  • Anyone who can reach the server can make it issue arbitrary outbound HTTP(S) requests, with the server's own IP/identity, to any host — including internal LAN addresses. The slide config already references an internal device (http://192.168.254.147/..., the 3D printer), confirming the internal network is reachable from this host.
  • Response bodies (including non-HTML content, per the early-return at line 197-198) are proxied back verbatim, so it can also be used to read responses from internal-only services.

Suggested fix

Restrict /proxy when called with a raw url param (the slide_id-driven path is fine, since that's server-controlled) to:

  • Only http/https schemes (already partially checked via urlparse(url).scheme, but doesn't reject other schemes like file:// explicitly)
  • Reject requests targeting private/loopback/link-local IP ranges (RFC1918, 127.0.0.0/8, 169.254.0.0/16, etc.), including after following redirects
  • Consider requiring the raw url/cookies path to be authenticated or removed entirely, since in practice every current caller goes through slide_id

Where

backend/app.py:160-222

## Problem `GET /proxy?url=<any-url>` (`backend/app.py:160-222`) fetches whatever URL is passed and returns the response, with no allowlist, no auth, and no restriction on scheme/host. It's reachable unauthenticated on the public HTTPS/HTTP listeners (`backend-tls-443.service` and `backend-http.service` both bind `0.0.0.0`). This makes the signage server an open SSRF proxy: - Anyone who can reach the server can make it issue arbitrary outbound HTTP(S) requests, with the server's own IP/identity, to any host — including internal LAN addresses. The slide config already references an internal device (`http://192.168.254.147/...`, the 3D printer), confirming the internal network is reachable from this host. - Response bodies (including non-HTML content, per the early-return at line 197-198) are proxied back verbatim, so it can also be used to read responses from internal-only services. ## Suggested fix Restrict `/proxy` when called with a raw `url` param (the `slide_id`-driven path is fine, since that's server-controlled) to: - Only `http`/`https` schemes (already partially checked via `urlparse(url).scheme`, but doesn't reject other schemes like `file://` explicitly) - Reject requests targeting private/loopback/link-local IP ranges (RFC1918, 127.0.0.0/8, 169.254.0.0/16, etc.), including after following redirects - Consider requiring the raw `url`/`cookies` path to be authenticated or removed entirely, since in practice every current caller goes through `slide_id` ## Where `backend/app.py:160-222`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
paul_loth/krautspaceTV#16
No description provided.