5.7 KiB
Security Best Practices Report
Executive Summary
This report originally identified three issues: cross-site WebSocket session hijacking, radio metadata SSRF, and an open cross-origin media proxy. All three have now been addressed in code with exact-origin enforcement via CHGRID_HOST_ORIGIN, SSRF-safe radio URL validation, and same-origin-only access for the PHP media proxy.
Critical Findings
SEC-001: Cross-site WebSocket hijacking via cookie-based session resume without Origin validation
Impact: A malicious website can open a WebSocket to the Chat Grid server from a victim's browser, inherit the victim's session cookie, and perform authenticated actions as that user.
Evidence:
- The server reads the session token directly from the WebSocket handshake cookie in server.py and server.py.
- The WebSocket client is auto-authenticated from that cookie before any application message is sent in server.py through server.py.
- There is no
Originvalidation in the handshake path around server.py.
Why this matters:
- Browser WebSockets are not protected by normal same-origin read restrictions once the server accepts the connection.
- Because the app uses cookie-based session resume, a third-party origin can potentially establish an authenticated socket unless the server rejects unexpected origins.
Implemented fix:
- The server now requires
CHGRID_HOST_ORIGINand passes that exact origin into the WebSocket handshake allowlist. - Browser sockets with missing or mismatched
Originare rejected before the application session resume path runs.
High Findings
SEC-002: Radio metadata polling creates server-side request forgery from user-controlled streamUrl
Impact: An authenticated user who can create or edit a radio item can cause the server to fetch attacker-chosen URLs, including internal network targets.
Evidence:
- Radio item validation accepts
streamUrlwith only length normalization in validator.py through validator.py. - The server later fetches that URL with
urllib.request.urlopenin server.py through server.py. - Any enabled radio with a listener in range is polled in server.py through server.py.
Why this matters:
- This is a classic SSRF primitive.
- A user does not need browser access to the target; the server makes the request.
- Internal services, metadata endpoints, localhost services, or other private hosts are not blocked here.
Implemented fix:
- Radio
streamUrlvalues are now validated server-side as publichttp/httpsURLs before they are saved. - Metadata polling revalidates and manually follows redirect hops so a safe initial URL cannot redirect into a blocked target.
Medium Findings
SEC-003: Optional PHP media proxy is an open cross-origin proxy when no allowlist is configured
Impact: If deployed as-is without CHGRID_MEDIA_PROXY_ALLOWLIST, third parties can use the site as a public cross-origin proxy to arbitrary public hosts.
Evidence:
- The proxy explicitly enables wildcard CORS in media_proxy.php through media_proxy.php.
- It accepts a user-supplied
urlparameter in media_proxy.php through media_proxy.php. - The hostname allowlist is optional, not required, in media_proxy.php through media_proxy.php.
- Redirect resolution and streaming then proceed for the target URL in media_proxy.php through media_proxy.php.
Why this matters:
- Even with private/reserved IP blocking, this can still be abused for bandwidth consumption, origin laundering, and relaying requests to arbitrary public endpoints.
- Because CORS is
*, other websites can read proxy responses directly from browsers.
Implemented fix:
- The proxy now requires
CHGRID_HOST_ORIGIN. Access-Control-Allow-Originis restricted to that exact origin, and mismatched request origins are rejected.- Upstream media hosts remain unrestricted, preserving the intended relay behavior for arbitrary public media sources.
Lower-Risk Notes
- I did not find obvious client-side DOM XSS in the current UI paths I sampled. The app mostly builds DOM with
textContentand node creation, and theinnerHTMLuses I saw were clearing containers rather than injecting untrusted markup. - The client no longer stores session tokens in
localStorage; session persistence is now moved to anHttpOnlycookie path, which is the right direction. - TLS posture appears intentionally gated by config, and I did not treat local insecure WebSocket support as a finding.
Residual Risks / Gaps
- I did not perform dependency CVE triage across
npmand Python package locks in this pass. - I did not run server-side tests or dynamic attack simulations; this was a code audit.
- The PHP proxy is deploy-time optional, but it is part of the repo and worth treating as production-facing if used.