Repository navigation
security(ws): nine WebSocket endpoints accept without authentication — PTY execution, workflow control and KB writes among them #17009
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Sep 18, 2026 - added 4 commits that reference this issue
on Sep 18, 2026 - added 5 commits that reference this issue
on Sep 19, 2026 All six acceptance criteria are verified against
mainat1b9f514328. The fix landed through #17048 (merge1b9f514328).AC1: each endpoint authenticates before accepting through
api/ws_security, and accepts throughaccept_websocket(). Met.- Every endpoint calls
open_authenticated_ws(api/ws_security.py:317). It checks origin, then authentication, thenadminand then the endpoint'sallowcheck, and only after all of them reachesaccept_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/researchapi/knowledge_research_ws.py:136authenticated 2 /api/overseer/ws/{session_id}api/overseer_handlers.py:451allow=owns_chat_session3 /api/workflow-automation/workflow_ws/{session_id}services/workflow_automation/ws_endpoint.py:44may_control(:32) plus slot claim4 /api/analytics/ws/realtimeapi/analytics.py:682authenticated 5 /api/analytics/ws/analytics/liveapi/analytics.py:1104authenticated 6 /api/quality/wsapi/analytics_quality.py:1682admin=True7 /api/logs/tail/{filename}api/logs.py:856admin=True8 /api/long-running/{operation_id}/progressapi/long_running_operations.py:407allow=_may_see_id9 /api/monitoring/realtimeapi/monitoring.py:1158authenticated 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)usesws_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.
may_control(ws_endpoint.py:32) allows an admin, or theowner_idthe workflow records.- No caller of
create_automated_workflowpassesowner_id(checkedroutes.py:71,api/templates.py:526,advanced_workflow/coordinator.py:146,manager.py:112), so control is admin-only today. - The owner model is filed separately as workflows: owner_id is never recorded, so workflow control cannot be scoped to its owner (REST control is open to any signed-in user) #17014, which is open.
WorkflowMessenger.claim_session(messaging.py:33) refuses a claim when another user holds the slot.
AC4 (row 8): progress goes to the creator or an admin. Met.
_may_see_id/_may_see(long_running_operations.py:99-126) allow themetadata["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, plusTestTheOverseerSocket,TestTheWorkflowSocketandtest_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 3test_a_slot_held_by_another_user_is_refused_and_its_socket_keptandtest_control_is_for_the_owner_or_an_admin; row 8 is covered insidetest_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.pydeclaresfloor=19(:134) and states at:130that 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.
- Every endpoint calls
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:
app_factoryhas nodependencies=, and routers are included without them).BaseHTTPMiddleware, which Starlette does not run for websocket scopes.enforce_ws_origin, which is a CSWSH Origin check only. A client that sends noOriginheader (curl, a script, another service) passes it./api/ws/knowledge/researchapi/knowledge_research_ws.py:139{"action":"start","query":…,"store":true}runslibrarian.research_query(store_quality_content=…): live web research and knowledge-base writes from attacker input/api/overseer/ws/{session_id}api/overseer_handlers.py:130type:"query"→orchestrate_executionthroughStepExecutorAgent(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-chosensession_id/api/workflow-automation/workflow_ws/{session_id}services/workflow_automation/routes.py:599accept()is the first statement. The caller-chosensession_idoverwrites another session's registered socket (terminal_sessions[session_id] = websocket), hijacking its slot.automation_controlpauses, resumes, cancels, approves (runs the next step) or skips steps of any workflow in the process-global manager/api/analytics/ws/realtimeapi/analytics.py:685/api/analytics/ws/analytics/liveapi/analytics.py:1108/api/quality/wsapi/analytics_quality.py:1005refreshand the initial send callget_quality_snapshot()as a plain function, skipping its RESTDepends(check_admin_permission). An admin-only snapshot, unauthenticated/api/logs/tail/{filename}api/logs.py:858/stream/{filename}is admin-gated (#744)/api/long-running/{operation_id}/progressapi/long_running_operations.py:506/api/monitoring/realtimeapi/monitoring.py:341update_intervalchanges the broadcast cadence for every clientAcceptance criteria
api/ws_security, and accepts throughaccept_websocket()(security(websocket): auth token travels in the URL query string, not a header/subprotocol #16457). Rows 6 and 7 require admin, matching their REST twins.session_idis bound to the authenticated user through the chat-session ownership check the terminal WebSocket uses. Another user's session is refused.Related
#17000, #17003, #16939, #16457, #14965