Skip to content

security(chat): the ownership gate still claims a session whose owner record is unreadable — #14033's contract is not honoured by SessionOwnershipValidator #17481

Description

@mrveiss

Found in review on #17426 (#17422), by CodeRabbit, and verified against current main.

Problem

#14033 gave ChatHistoryManager.get_session_owner an honest error contract: an unreadable session file raises SessionOwnerUnreadable (autobot-backend/chat_history/session.py, raise SessionOwnerUnreadable(session_id) from e) instead of returning None. api/conversation_files.py, api/ws_security.py and services/conversation_export.py catch it and treat it as unknown.

The chat ownership gate does not. SessionOwnershipValidator._durable_owner (autobot-backend/security/session_ownership.py) catches every exception and returns None:

except Exception as exc:
    logger.warning("Could not read the durable owner of session %s...: %s", session_id[:8], exc)
    return None

_owner_when_cache_is_empty then reads that None as "no owner anywhere" and writes the caller as owner (set_session_owner(session_id, username, ...)), returning legacy_migration. So a caller who reaches a conversation while its file is unreadable (decrypt failure, corruption, IO error) with no Redis record is granted ownership of it, and the grant persists in Redis.

The docstring calls this fail-open deliberate. It also contradicts #14033's acceptance criterion "an unreadable session file does not result in a claim", and #14033 is closed as completed.

Blast radius

Every route guarded by validate_ownership. The grant is also an input elsewhere: services/agent_terminal/conversation_owner.py falls back to it. #17426 hardens the agent-terminal claim path so that an unreadable owner refuses instead of trusting the grant. That PR does not fix the gate itself.

Not measured

  • How often an unreadable-but-existing session file occurs in practice.
  • Whether any caller depends on the fail-open. The docstring points to "the PR" for the reasoning; I did not locate it.

Suggested approach

_durable_owner distinguishes SessionOwnerUnreadable from None. On unreadable, _owner_when_cache_is_empty denies (or defers) instead of claiming, and never calls set_session_owner.

Acceptance criteria

  • An unreadable session file with no Redis record does not produce a claim and does not write an owner. The test asserts the exact status code.
  • A genuinely ownerless (readable) session still takes legacy_migration as today.
  • Mutation: restoring the return None makes the unreadable-file test fail.

Related

Activity

  1. mrveiss commented on Oct 10, 2026

    @mrveiss
    OwnerAuthor

    Witness from #14016's de-fork analysis: the conversation-files route check denies on an unreadable owner (#14033) while the canonical security/session_ownership.validate_session_ownership still allows and claims. That divergence is one of the 7 reasons #14016 can't merge the two validators until this is fixed.

  2. mrveiss commented on Oct 11, 2026

    @mrveiss
    OwnerAuthor

    Closure evidence (merged in merge train #18314, 5efe902; constituent #18258). Verified against main.

    • An unreadable owner yields no claim and no owner write, with the exact status asserted: autobot-backend/security/session_posture.py:73-87 read_owner turns any lookup failure into deny_unreadable_owner. session_posture_equivalence_14016_test.py:222-227 asserts 403 (_verdict maps 403 to DENY) and redis.set.assert_not_awaited() for all 5 failure rows under both postures; line 238 pins the count.
    • A genuinely ownerless session still takes legacy_migration: session_ownership.py:534; the test at :230-235 asserts ALLOW under both postures, with the claim written under DEFAULT only.
    • Mutation: met by inspection only. The except … return None path is gone from read_owner, and restoring it would turn :222's DENY into ALLOW. There is no mutation harness, and the merger did not run one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions