SECURITY: Python session scoping — owner-check only owned sessions (Option B) - #313
Merged
Conversation
…Option B) The th-8fe998 rule fail-closed on any principal with no email claim, which on an auth-enabled server denied anonymous connections and emailless authenticated principals access to the sessions they had just created themselves. The same rule in .NET hung CI and was reverted in #309. Option B: an OWNED session is still owner-checked; an OWNERLESS one (anonymous / emailless / legacy) is reachable as before. An emailless scope matches no non-empty owner, so authenticated A still cannot read, resume, or send into authenticated B's owned session, and a refusal appends nothing to B's log. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01331RwggrhiP9UJVao9dr1F
🦋 Changeset detectedLatest commit: b6eed9d The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the P0 lockout introduced with the Python half of the conversation-scoping fix (#295/#304, th-8fe998). Pearl th-909995.
The bug
_visible_sessiondenied whenever the principal had no email claim. On an auth-enabled server that means an anonymous connection, or a token carryingsub/org/rolebut noemail, is denied everything — including the session it just created (which is ownerless by construction): emptylist_conversations, resume mints a fresh conversation,get_session/get_conversation_messages/send_messageall answerSESSION_NOT_FOUND. That is "cannot use the product" for anonymous/public-agent chat.The identical rule in .NET hung CI on
Acl_PrivateDoc_OnlyReachesEntitledUser_OverWebSocket(emailless token + anonymous second connection) and was reverted in #309. Neither Python nor Rust had an equivalent test, which is why it went unnoticed here.The fix — Option B
A session that has an owner is owner-checked (case/whitespace-insensitive email match). A session with no owner — anonymous, emailless, or predating ownership — is reachable, exactly as before scoping shipped. An emailless scope matches no non-empty owner, so the reported P0 stays closed.
Option A (
email ?? sub) was rejected: Go's anonymous principal uses the literal sub"anonymous"for every visitor, so keying onsubpools all anonymous visitors and leaks their chats to each other.SessionStore.create_session/list_conversationsgain a keyword-onlyenforced: bool = False— the store previously had no way to tell "auth disabled, unscoped" from "authenticated but emailless" (both present as aNoneowner). Without it the resume path would have let an emailless principal bind to anyone's conversation.Kept
_visible_sessionremains the ONLY sessionId lookup, andtest_visible_session_is_the_only_lookup(exactly one directself._store.get_session(call site) still guards it.Tests
New/changed in
test_conversation_scoping.py:test_emailless_principal_can_use_its_own_session(anonymous-on-auth-enabled and emailless-authenticated): create →get_session→send_message(reachesLLM_UNAVAILABLE, i.e. past the ACL gate) →list_conversations→ resume by conversationId.test_emailless_principal_still_cannot_reach_an_owned_session— the permissive half is only the ownerless case.test_send_message_never_appends_to_another_users_session— the reported P0, asserted on B's log, not just the error code.test_unowned_conversation_is_reachable_by_every_principal(replaces the old "invisible to every principal").test_store_enforced_scope_admits_ownerless_only_alongside_the_owner— store-levelenforcedmatrix.Verified the new cases FAIL against the old predicate: restoring it locally fails 4 of them (
test_emailless_principal_can_use_its_own_session[emailless-principal],[anonymous-enforced],test_unowned_conversation_is_reachable_by_every_principal,test_store_enforced_scope_admits_ownerless_only_alongside_the_owner) while the P0 guards keep passing.Gates:
uv run pytest207 passed (python/server) + 52 passed 1 skipped (python);ruff checkandruff format --checkclean in both.🤖 Generated with Claude Code
https://claude.ai/code/session_01331RwggrhiP9UJVao9dr1F