Skip to content

security(chat): export_session has no ownership check — any authenticated user can export another user's full transcript #14011

Description

@mrveiss

Found while verifying the endpoint audit in PR #14009 (#13982). Same vulnerability class, different router — and unlike the flag-gated checks, this one is simply never called.

Problem

export_session (autobot-backend/api/chat_sessions.py, GET /chat/sessions/{session_id}/export) takes session_id from the path and calls neither validate_session_ownership nor validate_chat_ownership anywhere in its body:

async def export_session(session_id: str, request: Request, format: str = "json"):
    _validate_session_id_or_raise(session_id)
    _validate_export_format_or_raise(format)
    ...
    session_data = await _export_session_data_or_raise(chat_history_manager, session_id, format)

The only guards are a session-id format check and a format check. Verified by reading the full function, not by grepping for the word "ownership".

Impact

Any authenticated user who knows or guesses another user's session_id can export that user's entire chat transcript. This is bulk disclosure of conversation content, which is a worse outcome than the injection gap #13982 covered, and it is not feature-flag dependent — there is no check to enable.

Session ids are the same values that appear in URLs and are referenced across the chat APIs, so "knows or guesses" is not a high bar in practice.

Suggested fix

Add the ownership check the sibling endpoints use, before any data is read:

await validate_chat_ownership(session_id, request)  # SECURITY: caller must own the session

Then audit the rest of api/chat_sessions.py the same way — by reading each function that accepts a session_id, not by grepping. #14009's audit covered api/chat.py only; the assumption that a sibling router was equally covered turned out to be wrong, which is the reason to check the rest by hand.

Acceptance criteria

  • export_session refuses a session_id the caller does not own
  • Test: user A cannot export user B's session; the owner still can (behavioural, not inspect.getsource)
  • Every other endpoint in api/chat_sessions.py that accepts a session id is audited by reading the function body; the result is listed in the PR, including any found unprotected
  • Any further gaps found are fixed in the same PR or filed

Note on enforcement

Adding the call closes the missing-check gap, but the shared validator currently fails open — see #14010. Both are needed for this endpoint to actually refuse a non-owner in a default deployment.

Related

Activity

  1. mrveiss commented on Aug 11, 2026

    @mrveiss
    OwnerAuthor

    Delivered by PR #14013, merged as e03a8198e.

    Verified in the base branch

    $ git log origin/Dev_new_gui --oneline --grep=14011
    e03a8198e security(chat): require ownership on export_session and reset_chat (#14011) (#14013)
    
    export_session  Depends(validate_session_ownership)  -> present
    reset_chat      await validate_session_ownership(...) -> present
    

    Acceptance criteria — all four met

    AC how
    export_session refuses a session the caller does not own Depends(validate_session_ownership), the pattern the other eight endpoints in the file use
    test: user A cannot export user B's session; the owner still can — behavioural request-level tests via TestClient on a mounted router; a non-owner gets 403, the owner does not
    every other endpoint in the file audited by reading the body 13 endpoints; results in the PR
    further gaps fixed in the same PR or filed reset_chat fixed here; create_session filed as #14012 and since fixed in PR #14017

    What the audit turned up beyond the filed issue

    Corrections I made along the way

    My first version of the export tests asserted on inspect.signature, which does not meet this issue's own AC ("behavioural, not inspect.getsource") — and could not have caught the failure most worth catching, a dependency binding session_id from the query string instead of the path. Replaced with request-level tests.

    Before treating these endpoints as protected

    Both checks route through the shared validator, which fails open (#14010) and whose ownership record expires after 24 hours and is then claimed by whoever accesses the session next (#14018). In a default deployment these calls are inert. They are correct and necessary; the enforcement posture is what decides whether they bite.

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

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions