Skip to content

security(chat): session ownership filter fails OPEN — a Redis error returns every session on the instance #12867

Closed

Description

@mrveiss

Split out of #12685 so the delivered work has a closable scope. #12685 stays open for the broader tenancy design.

Problem

_filter_user_sessions (api/chat_sessions.py:438) is the isolation filter on GET /api/chat/sessions. It failed open in two places:

user_session_ids = set(await validator.get_user_sessions(username))
if not user_session_ids:
    return sessions          # (a) no ownership data -> return EVERY session
...
except Exception as e:
    logger.debug(...)
    return sessions          # (b) lookup failed -> return EVERY session

Path (b) is indefensible: a Redis blip silently returns every session on the instance — including other companies' agent conversations. This is a plausible mechanism for the live observation in #12685, where a CEO · <company_id> session appeared in an ordinary user's chat list. It also failed at logger.debug, so it left no visible trace.

Fix

Path (b) now raises 503 OwnershipUnavailableError instead of returning an unfiltered list. 503 rather than 500 because the request is valid and will succeed once the ownership store is reachable; the UI can say "could not load your sessions" instead of showing someone else's.

Path (a) is deliberately left as-is and is the open question below.

Open decision (path a)

A user with no ownership records currently sees the unfiltered list. Failing closed there would be consistent — but on installs predating ownership tracking it would hide users' own history, so it is not a safe unilateral change.

Options:

  1. Fail closed — return an empty list. Correct for isolation; risks legacy sessions becoming invisible.
  2. Backfill ownership for existing sessions, then fail closed. Safest but needs a migration.
  3. Keep open, now logged (current state) — the exposure is at least visible in logs rather than silent.

Done when

  • A failed ownership lookup never returns an unfiltered list.
  • The no-records path has an explicit decision recorded, not an accidental default.

Activity

  1. github-actions commented on Jul 28, 2026

    @github-actions
    Contributor

    PR #12868 (merged to Dev_new_gui) references this issue with a close keyword.

    security(chat): ownership filter no longer returns every session when the lookup fails (#12867)

    If this issue is fully resolved, close it manually. If work remains, no action is needed.

  2. mrveiss commented on Jul 28, 2026

    @mrveiss
    OwnerAuthor

    Fixed in PR #12868 — squash-merged to Dev_new_gui as 01cb31c1636c68d8ef341185edabe36b3d3d4d87.

    The lookup-failure path now raises 503 OwnershipUnavailableError instead of returning the unfiltered session list. 503 rather than 500 because the request is valid and will succeed once the ownership store is reachable — the UI can say "could not load your sessions" instead of showing someone else's.

    It previously failed at logger.debug, so a Redis blip exposed every session on the instance with no visible trace.

    Path (a) — no ownership records for the user — is deliberately unchanged and now logs a warning rather than being silent. Failing closed there would hide users' own history on installs predating ownership tracking, so it needs the owner decision recorded in this issue (fail closed / backfill then fail closed / keep open). A test pins the current behaviour so the deferred decision cannot be forgotten.

    Verification: 5 new tests — only own sessions returned, the CEO agent session excluded when unowned, a store failure raises 503 rather than returning everything, a validator failure likewise, and the legacy no-records path still returns all but warns. 387 passed across the chat/session selection.

  3. mrveiss commented on Jul 29, 2026

    @mrveiss
    OwnerAuthor

    Confirmed path (b) is delivered on the current tip — _filter_user_sessions raises OwnershipUnavailableError and logs at error (api/chat_sessions.py:449-461). This issue is correctly still open only for the path (a) decision.

    Small residue worth folding into whichever PR settles that decision: the function's docstring still documents the removed behaviour.

    async def _filter_user_sessions(sessions: list, username: str) -> list:
        """Filter sessions to those owned by the user.
    
        Uses Redis ownership data. Falls back to all sessions
        if Redis is unavailable.          # <- no longer true; it now raises 503
        """

    Worth correcting because this is a security boundary: the next reader to reason about failure modes from the docstring would conclude the filter still fails open. Not filing separately — it belongs with the path (a) change that will touch these lines anyway.

    Path (a) itself remains an owner call between the three recorded options (fail closed / backfill-then-fail-closed / keep open but logged).

  4. mrveiss commented on Jul 30, 2026

    @mrveiss
    OwnerAuthor

    Resolved — closing with merge evidence.

    Fixed by PR #12868, merged to Dev_new_gui as 01cb31c1636c68d8ef341185edabe36b3d3d4d87 (2026-07-28). Verified present on the current branch tip, not merely reported merged.

    • autobot-backend/api/chat_sessions.py — imports and applies security.session_ownership.validate_session_ownership, so a Redis error can no longer fall through to returning every session.
    • autobot-backend/api/chat_sessions_errors.py — shared error handling for the fail-closed path.
    • autobot-backend/tests/test_chat_session_ownership_failclosed.py — present on tip (blob 24d6f54b), pinning the fail-closed behaviour so a regression is caught rather than silently reopening the hole.

    Left open only because Closes #N fires on the default branch (main) and this merged to Dev_new_gui, so GitHub never auto-closed it.

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