Skip to content

security(websocket): /api/intelligent_agent/stream runs agent goals with no authentication — an unauthenticated bypass of POST /process #17000

Description

@mrveiss

What

The intelligent agent's WebSocket, /api/intelligent_agent/stream (autobot-backend/api/intelligent_agent.py, websocket_stream, around line 254), accepts any client without authentication and runs the goal it is sent:

if not await enforce_ws_origin(websocket):
    return
await websocket.accept()
...
data = await websocket.receive_json()
...
async for chunk in agent.process_natural_language_goal(goal, context=context):
  • enforce_ws_origin is the only check. By its own docstring it returns True when the handshake origin is allowed or absent, and a non-browser client simply omits the Origin header.
  • intelligent_agent.router is APIRouter(tags=[...]) with no dependencies, and its registry entry (initialization/router_registry/core_routers.py, _get_agent_routers) adds none.
  • The HTTP twin of this operation, POST /api/intelligent_agent/process, does require Depends(get_current_user). The WebSocket is therefore a direct unauthenticated bypass of an authenticated route. The endpoint's docstring says clients "send natural language goals and receive real-time updates as the agent processes and executes commands".

The deployed backend runs the same code (host evidence, read-only, 2026-09-18).

Found during the independent review of #16939 (WebSocket subprotocol auth), which enumerated the raw websocket.accept() sites that call no authenticator.

Acceptance criteria

  • /stream authenticates before processing anything, with at least the same principal requirement as POST /process (a verified user). An unauthenticated or invalid-token handshake is closed with a policy-violation close code before any receive_json.
  • The goal runs under the authenticated principal: its identity is passed to the agent the same way /process passes it, never anonymously.
  • Tests: no token is refused, an invalid token is refused, a valid token is accepted (using the repository's existing WebSocket-auth test pattern), and a refused handshake never reaches process_natural_language_goal (negative control).
  • The broader sweep of the other WebSocket endpoints that call no authenticator is tracked in Per-route WebSocket auth sweep with a route-level (not file-level) guard #14965, and this endpoint is covered by that issue's guard once it lands.

Related

Activity

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

    @mrveiss
    OwnerAuthor

    Re-verified against current origin/main (adc70bcfc5, post-#17086 merge) — not just the earlier PR-level review. Fix landed in 6e1d86baa5 ("security(websocket): authenticate /api/intelligent_agent/stream before processing (#17000)").

    AC checklist (current main)

    • Auth before processing, policy-violation close before receive_json — autobot-backend/api/intelligent_agent.py:275-282: enforce_ws_origin → authenticate_websocket(websocket) → if user is None, websocket.accept() + websocket.close(code=4001, reason="Authentication required") + return, before get_agent() (line 285) or receive_json() (line 289). Matches the accept-then-close-4001 convention (voice_stream_ws). authenticate_websocket (autobot-backend/auth_middleware.py:1053-1086) does real JWT verification via the singleton AuthenticationMiddleware, returns None on missing/invalid token — not a stub.
    • Goal runs under the authenticated principal, never anonymously — same parity as POST /process (intelligent_agent.py:115-118, gated by Depends(get_current_user)): neither route forwards the user identity into agent.process_natural_language_goal(...) beyond gating access; /stream now requires the identical non-None user before entering the receive loop (intelligent_agent.py:277-286), so it's no longer reachable anonymously.
    • Tests intact — autobot-backend/api/intelligent_agent_ws_auth_17000_test.py present on current main: test_no_token_is_refused and test_invalid_token_is_refused (both assert WS close code 4001 and mock_get_agent.assert_not_awaited()), test_valid_token_is_accepted_and_reaches_the_agent (asserts process_natural_language_goal called once), and the negative control test_a_refused_handshake_never_calls_process_natural_language_goal proving get_agent is never awaited when auth fails.
    • Broader sweep tracked — Per-route WebSocket auth sweep with a route-level (not file-level) guard #14965 ("Per-route WebSocket auth sweep with a route-level guard") is open and tracks the remaining endpoints; intelligent_agent.py has exactly one @router.websocket route (line 254), now fixed.

    Verdict

    All 4 ACs verified met on current origin/main with code evidence — the merge of #17086 did not drop or reorder the fix. Auth-before-processing sequence is intact end-to-end. No other WebSocket route in intelligent_agent.py has this gap. Leaving closed.

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