Repository navigation
security(chat): require chat ownership on /chat/direct (#13982) - #14009
Conversation
send_direct_chat_response took chat_id from the request body and validated
nothing, while its sibling /chats/{chat_id}/message declares
Depends(validate_chat_ownership). Authentication was required, so this was never
anonymous — the missing boundary was between authenticated users.
It matters more than message injection because this endpoint carries approval and
denial decisions: any authenticated user could resolve another user's pending
command approval and, with remember_choice, persist that decision for their
future turns.
Depends(validate_chat_ownership) cannot be reused here — it resolves chat_id as a
path parameter and this endpoint takes it from the body, so the dependency would
validate a different value than the one used. The explicit call is the same
pattern the nine session endpoints above already use, so this adds no mechanism.
Audited every chat endpoint that accepts a chat/session id: 14 of them, and this
was the only one unchecked. Both process_message_stream entrypoints are now
covered.
Tests assert behaviour, not that a string appears in the source — the existing
migration tests use inspect.getsource, which passes on a check that is present
but never reached. One test pins that the id validated is the id acted on, which
is the failure the body-vs-path mismatch would have produced.
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
Review: SAFE — with two adjacent gaps now filed, and one correction to my auditThe diff verifies. Correction to my audit — 15 endpoints, not 14I missed Two gaps found while verifying my claims — filed, not folded in#14011 — That one is on me: my audit covered #14010 — ownership enforcement fails open. This means the ~15 calls in I did not fold either into this PR. #14010 changes deployment-wide authorization posture and will start refusing requests that work today; that needs its own rollout ( Not changed
|
Auto-regenerated (py3.14) to match the backend schema so the required verify-generated-types gate passes. Triggered by auto-fix-generated-types.yml.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Closes #13982
Thinking Path
send_direct_chat_responsetookchat_idfrom the request body and validated nothing, while its sibling/chats/{chat_id}/messagedeclaresDepends(validate_chat_ownership). Authentication was required, so this was never anonymous — the boundary that was missing is the one between authenticated users, which is exactly whatvalidate_chat_ownershipexists to enforce.It matters more than message injection because this endpoint carries approval and denial decisions. Reachable consequences:
remember_choice=Truepersists that decision for their future turnsDecision: an explicit call, not a dependency
Depends(validate_chat_ownership)looks like the obvious fix and is wrong here. That dependency resolveschat_idas a path parameter; this endpoint takes it from the body. FastAPI would therefore validate a different value than the one the request acts on — a check that passes while guarding nothing, which is worse than no check because it reads as covered.The explicit call is the pattern the nine session endpoints above already use (
await validate_chat_ownership(session_id, request) # SECURITY: ...), so this adds no new mechanism and reuses the canonical validator with its feature-flag, metrics and audit integration.What Changed
api/chat.py—await validate_chat_ownership(chat_id, request)insend_direct_chat_response, placed before any workflow work.Verification
Audit (AC 4)
Every endpoint in
api/chat.pythat accepts a chat/session id — 14 of them. This was the only one unchecked:Both
process_message_streamentrypoints are now covered.Tests
Four new tests, asserting behaviour rather than source text. The existing precedent (
api_endpoint_migrations_test.py) usesinspect.getsource+assertIn("validate_session_ownership", ...), which passes on a check that is present but never reached — the exact failure mode worth guarding against here.The tests that carry the most weight:
remember_choicepersists a decision, so a check that runs after the side effect is not a checkchat_idis the one actually used — pins the body-vs-path mismatch described aboveMutation-tested:
A note on that second row, because my first run reported it as SURVIVED: my replacement anchor was missing a blank line, so the edit silently did nothing and the harness read "no test failed" as "the mutant lived". With the patch actually applied it is killed. A mutation harness that does not verify its own edit landed will report no-ops as survivors — worth knowing before adding tests to chase a phantom gap.
Lint: black, isort, flake8 clean.
Not covered
The fix relies on
validate_chat_ownershipbehaving correctly; its enforcement is feature-flagged (SessionOwnershipValidatordegrades to permissive if the flags service is unavailable). That pre-existing behaviour is unchanged here — if the flag is off in a given deployment, this endpoint is now consistent with the other 13 rather than uniquely open, but the deployment-level posture is a separate question from this issue.Model Used
Claude Opus 5