Skip to content

security(ws): nine WebSocket endpoints accept without authentication — PTY execution, workflow control and KB writes among them #17009

Description

@mrveiss

Found in the WebSocket auth triage that followed #17000 and #16939's review. Coordinator confirmed the verdicts.

Problem

Nine WebSocket endpoints accept a connection without authenticating it. Nothing else covers them:

  • No router or app-level dependency applies (app_factory has no dependencies=, and routers are included without them).
  • Every HTTP middleware is a BaseHTTPMiddleware, which Starlette does not run for websocket scopes.
  • At most, an endpoint calls enforce_ws_origin, which is a CSWSH Origin check only. A client that sends no Origin header (curl, a script, another service) passes it.
# Path Accept site What an unauthenticated client can do / see Severity
1 /api/ws/knowledge/research api/knowledge_research_ws.py:139 {"action":"start","query":…,"store":true} runs librarian.research_query(store_quality_content=…): live web research and knowledge-base writes from attacker input critical
2 /api/overseer/ws/{session_id} api/overseer_handlers.py:130 type:"query" → orchestrate_execution through StepExecutorAgent(pty_session_id=session_id): plans and runs commands in a PTY. Also cancel, and writes into that session's chat history. Sessions are a process-global dict keyed by the caller-chosen session_id critical
3 /api/workflow-automation/workflow_ws/{session_id} services/workflow_automation/routes.py:599 No check at all, not even Origin. accept() is the first statement. The caller-chosen session_id overwrites another session's registered socket (terminal_sessions[session_id] = websocket), hijacking its slot. automation_control pauses, resumes, cancels, approves (runs the next step) or skips steps of any workflow in the process-global manager critical
4 /api/analytics/ws/realtime api/analytics.py:685 Global telemetry: API call patterns and endpoints, latencies, CPU, memory, disk high
5 /api/analytics/ws/analytics/live api/analytics.py:1108 Same metrics, plus health and alerts high
6 /api/quality/ws api/analytics_quality.py:1005 refresh and the initial send call get_quality_snapshot() as a plain function, skipping its REST Depends(check_admin_permission). An admin-only snapshot, unauthenticated high
7 /api/logs/tail/{filename} api/logs.py:858 Live tail of any file under the log directory. The REST twin /stream/{filename} is admin-gated (#744) high
8 /api/long-running/{operation_id}/progress api/long_running_operations.py:506 Any operation's status, error message and metadata, with no ownership check high
9 /api/monitoring/realtime api/monitoring.py:341 System CPU, memory, GPU, NPU and service health. update_interval changes the broadcast cadence for every client high

Acceptance criteria

Related

#17000, #17003, #16939, #16457, #14965

Activity

  1. added this to the v0.9.0 milestone on Sep 18, 2026
  2. mrveiss commented on Sep 19, 2026

    @mrveiss
    OwnerAuthor

    All six acceptance criteria are verified against main at 1b9f514328. The fix landed through #17048 (merge 1b9f514328).

    AC1: each endpoint authenticates before accepting through api/ws_security, and accepts through accept_websocket(). Met.

    • Every endpoint calls open_authenticated_ws (api/ws_security.py:317). It checks origin, then authentication, then admin and then the endpoint's allow check, and only after all of them reaches accept_websocket(websocket) (:349). Rejections close without accepting.
    • None of the 8 handler modules has a bare websocket.accept( (count 0 in each).
    # Endpoint Gate call Check
    1 /api/ws/knowledge/research api/knowledge_research_ws.py:136 authenticated
    2 /api/overseer/ws/{session_id} api/overseer_handlers.py:451 allow=owns_chat_session
    3 /api/workflow-automation/workflow_ws/{session_id} services/workflow_automation/ws_endpoint.py:44 may_control (:32) plus slot claim
    4 /api/analytics/ws/realtime api/analytics.py:682 authenticated
    5 /api/analytics/ws/analytics/live api/analytics.py:1104 authenticated
    6 /api/quality/ws api/analytics_quality.py:1682 admin=True
    7 /api/logs/tail/{filename} api/logs.py:856 admin=True
    8 /api/long-running/{operation_id}/progress api/long_running_operations.py:407 allow=_may_see_id
    9 /api/monitoring/realtime api/monitoring.py:1158 authenticated

    Rows 6 and 7 match their REST twins' Depends(check_admin_permission).

    AC2 (row 2): session bound to the authenticated user. Met. allow=lambda user: owns_chat_session(websocket, session_id, user) uses ws_security.py:363: the Redis owner, else the durable session-file owner. Unowned or unreadable is refused, and admins bypass.

    AC3 (row 3): control only for the owner or an admin, and no slot takeover. Met, as the AC's own fallback.

    AC4 (row 8): progress goes to the creator or an admin. Met. _may_see_id/_may_see (long_running_operations.py:99-126) allow the metadata["created_by"] creator or an admin. An unrecorded creator means admin-only.

    AC5: tests for each endpoint. Met, in autobot-backend/api/ws_auth_17009_test.py (17 test functions, some parametrized over the 6 table rows).

    • Unauthenticated refused: test_an_unauthenticated_client_is_refused_before_accept[*] for rows 1, 4, 5, 6, 7 and 9, plus TestTheOverseerSocket, TestTheWorkflowSocket and test_operation_progress_is_for_its_creator_or_an_admin.
    • Authenticated allowed: test_the_right_caller_is_accepted[*], plus the class equivalents.
    • Cross-user: row 2 test_another_users_session_is_refused; row 3 test_a_slot_held_by_another_user_is_refused_and_its_socket_kept and test_control_is_for_the_owner_or_an_admin; row 8 is covered inside test_operation_progress_is_for_its_creator_or_an_admin.
    • Admin-only rows also have test_a_non_admin_is_refused_where_the_rest_twin_is_admin_only[quality|log tail].

    AC6: #16939's echo guard covers every endpoint here, with no exemptions. Met.

    • repo_tests/websocket_subprotocol_echo_guard_test.py declares floor=19 (:134) and states at :130 that all the newly authenticating modules "are in the population, with no exemption".
    • The module has no allowlist or exemption mechanism: I read the whole file and confirmed the absence.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions