Conversation
…r permissions The Teams card-action handler trusted both session_key and hermes_action from the Action.Execute invoke payload, which round-trips through the Teams client and is untrusted. Two holes: a crafted invoke could approve a DIFFERENT session's pending dangerous command (cross-session IDOR — every sibling adapter binds one of clicker/session, Teams bound neither), and could resubmit choices the render side withheld (approve_always despite allow_permanent=False / smart_denied) — writing the dangerous pattern to the permanent allowlist on disk. Now: the session_key must contain the invoking conversation's id, and choices are enforced against the pending approval's recorded permissions via a new get_blocking_approval_data() helper.
|
Thanks for addressing a real Teams approval-boundary gap. Current main accepts Problems
Suggested changes
Automated hermes-sweeper review. |
Addresses review on 1817e86: the conversation binding was a substring test (session_key contains conversation.id), which can match unrelated portions of the opaque key — Teams conversation ids themselves contain ':' so the structured key segments are ambiguous. And the permission read (get_blocking_approval_data) + later FIFO resolve were two separate lock holds, so concurrent invokes against a multi-entry queue could validate one entry and pop another. Now: _ApprovalEntry carries an unguessable uuid (stamped into approval_data for the notify path), the card payload embeds ONLY that approval id (session_key no longer round-trips through the Teams client), and send_exec_approval binds the entry to the render-time conversation id server-side. _on_card_action calls the new resolve_gateway_approval_by_id(), which under a single _lock hold finds the exact entry, requires an exact conversation-id match, enforces the render-time permissions (smart_denied / allow_session / allow_permanent), and removes exactly that entry — concurrent invokes cannot cross-consume. gateway/run.py forwards approval_id to adapters that opt in via the kwarg (signature check, other adapters unaffected). Regression tests: two-entry/concurrent-invoke with differing permission flags, valid-id-withheld-choice rejection, substring conversation mismatch, forged approval id, session_key-only legacy payload, and renderer payload (approval id embedded, session_key absent).
|
Reworked in eff4239. The substring binding is gone: each Regression coverage added: a two-entry/concurrent-invoke test with differing permission flags (each invoke consumes only its own entry), a valid-id-withheld-choice rejection, a substring conversation-mismatch rejection, and a forged-id rejection. Verification: tests/gateway/test_teams.py 30/30 pass; approval-adjacent suites (discord/feishu/matrix/slack/telegram/tui + approvals commands) green; revert-verified — the 10 security tests fail on pre-fix main and pass with the change. |
|
looks mergeable The replayed Teams approval change closes the prior cross-session and withheld-choice paths: card invokes carry only an opaque queue-entry id, the server binds it to the render-time conversation, and one locked resolver validates permissions before signaling the exact waiting entry. No additional source-backed security issue was found. Security evidence:
Review setup: I reviewed a run-owned local rebase or patch replay against current GitHub Not checked:
Signed: GPT-5.6-luna-max in Codex |
What does this PR do?
Fixes two approval-escalation holes in the Teams card-action handler. The Action.Execute invoke payload round-trips through the Teams client and is untrusted (Microsoft's own guidance), but the handler trusted both session_key and hermes_action from it: a crafted invoke could approve a DIFFERENT session's pending dangerous command (cross-session IDOR — QQ and Feishu bind the clicker to the session; Teams bound nothing), and could resubmit choices the render side withheld (approve_always despite allow_permanent=False or smart_denied) — which writes the dangerous pattern into the permanent on-disk allowlist. The session_key must now contain the invoking conversation's id, and every choice is enforced server-side against the pending approval's recorded permissions (allow_session / allow_permanent / smart_denied) via a new get_blocking_approval_data() helper in tools/approval.py.
Related Issue
No GitHub issue — discovered via code review and reproduced live (see below). Happy to file one first if preferred.
Changes Made
fix/teams-approval-idor— 3 file(s) changed vs base:plugins/platforms/teams/adapter.pytests/gateway/test_teams.pytools/approval.pyHow to Test
Validation completed (recorded by prp):
tests/gateway/test_teams.py.tests/gateway/test_teams.py tests/tools/test_approval.py tests/tools/test_approval_heartbeat.py: branch 114 passed / 0 failed vs baseline 110 passed / 0 failed — zero branch-only failures.Logs
Sabotage verification output: