The #832 fix enforces the launch PIN / login via a Flask before_request hook, but that hook does NOT run for the socketio handshake — empirically a normal endpoint 401s while /socket.io/ returns 200 with the gate on. So removing the client overlay (Safari "Hide Distracting Items", devtools) + opening a socket streams live data (downloads, logs, dashboard, notifications) completely unauthenticated. Fix: the socketio connect handler now enforces the same check and returns False (rejects the connection) when a gate is active and the session isn't verified. Rejecting connect blocks every downstream WS event (subscribe/join), so all live data is covered. core/security/ws_gate.is_ws_connection_blocked is the pure seam: login mode (when on) > launch PIN > open, mirroring the HTTP gate exactly. Fails OPEN on a config-read error, same as the HTTP gate. Audited every other surface empirically with the gate on + unauthenticated: SSE streams, catch-all pages, library/dashboard data, admin endpoints, search, image-proxy, audio-stream (incl. a /etc/passwd traversal probe) all 401; /api/v1 key-gated. The WebSocket was the only hole. Tests (10): pure gate logic (login>pin precedence, all on/off combos) + real socketio.test_client integration — connect rejected when gate on + unauthenticated, allowed when gate off or PIN verified.
39 lines
1.3 KiB
Python
39 lines
1.3 KiB
Python
"""WebSocket access gate (#852).
|
|
|
|
Flask's ``before_request`` — where the launch-PIN / login gate lives — does NOT
|
|
run for the socketio handshake, so an unauthenticated client that removes the
|
|
login/PIN overlay (Safari "Hide Distracting Items", devtools, curl) can still open
|
|
a socket and receive the live data SoulSync streams over it (downloads, logs,
|
|
dashboard, notifications). The connect handler must therefore enforce the same
|
|
check the HTTP gate does.
|
|
|
|
Pure decision so it's unit-testable; the socketio handler injects the live
|
|
session/config/header values. Mirrors the HTTP gate precedence exactly: login
|
|
mode (when on) replaces the launch PIN.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
def is_ws_connection_blocked(
|
|
*,
|
|
require_login: bool,
|
|
login_authenticated: bool,
|
|
require_pin: bool,
|
|
pin_verified: bool,
|
|
proxy_authed: bool,
|
|
) -> bool:
|
|
"""True ⇒ reject this WebSocket connection.
|
|
|
|
- Login mode on → must be login-authenticated.
|
|
- Else PIN on → must have verified the PIN (or be trusted by an auth proxy).
|
|
- Neither on → open (matches the HTTP gate's no-op default).
|
|
"""
|
|
if require_login:
|
|
return not login_authenticated
|
|
if require_pin:
|
|
return not (pin_verified or proxy_authed)
|
|
return False
|
|
|
|
|
|
__all__ = ["is_ws_connection_blocked"]
|