Skip to content

[security] fix(qqbot): bind approval buttons to initiating user#24437

Closed
Hinotoi-agent wants to merge 1 commit into
NousResearch:mainfrom
Hinotoi-agent:fix/qqbot-approval-authorization
Closed

[security] fix(qqbot): bind approval buttons to initiating user#24437
Hinotoi-agent wants to merge 1 commit into
NousResearch:mainfrom
Hinotoi-agent:fix/qqbot-approval-authorization

Conversation

@Hinotoi-agent

@Hinotoi-agent Hinotoi-agent commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR hardens QQBot dangerous-command approval buttons so a group-visible inline keyboard cannot be used by a different group member to approve another user's pending command.

  • Binds each QQBot button-based exec approval to the initiating operator and chat context.
  • Rejects group button clicks from the wrong operator, chat, scene, expired approval context, or unbound group approvals.
  • Passes the initiating source.user_id from the gateway runner into the adapter's approval metadata.
  • Adds focused regression tests for unauthorized group-member rejection, authorized operator approval, metadata binding, and safe fallback when no group operator is known.

Security issues covered

Issue Impact Severity
QQBot group approval buttons were resolved from button payload alone Any group member who could see the dangerous-command approval keyboard could click allow-once, allow-always, or deny for another user's pending command High in mixed-trust QQ group deployments

Before this PR

  • QQBot approval buttons embedded only approve:<session_key>:<decision> in the button payload.
  • QQ group keyboards are visible/clickable by group members, but _default_interaction_dispatch() did not verify the clicker's operator_openid before resolving the approval.
  • The dispatcher logged the operator identity but still called resolve_gateway_approval(session_key, choice) solely from button data.
  • There were no QQBot regression tests proving that a different group member cannot approve another user's dangerous command.

After this PR

  • send_exec_approval() records an in-memory approval context containing:
    • expected operator openid
    • expected chat id
    • expected chat type
    • creation time
  • The default QQBot interaction dispatcher checks that context before resolving dangerous-command approvals.
  • Group approval buttons without an expected operator fail safely so the gateway can fall back to the text approval flow.
  • Approval context is removed after successful resolution, failed inline send, or expired click rejection.
  • Tests lock in both the reject and allow paths.

Why this matters

Dangerous-command approval is a human-in-the-loop security boundary. In QQ group chats, the approval prompt may be visible to multiple people, but the pending command belongs to the user/session that triggered it.

Without checking the clicker identity, the boundary becomes "anyone who sees the group button can approve." The highest-impact case is a bystander clicking allow-always, which maps to the approval subsystem's persistent always decision.

How this differs from #18180

#18180 hardened Telegram inline approval callbacks by enforcing Telegram callback authorization through gateway user policy.

This PR fixes the same trust-boundary class for a different platform and code path:

  • different adapter: gateway/platforms/qqbot/adapter.py
  • different event type: QQ INTERACTION_CREATE
  • different identity field: QQ group_member_openid / operator_openid
  • different UI behavior: QQ group inline keyboards can be visible/clickable by group members
  • different tests: focused QQBot interaction-dispatch regression tests

The Telegram fix does not cover QQBot's inline keyboard dispatcher.

Attack flow

QQ group member sees another user's dangerous-command approval prompt
    -> clicks the inline "allow always" button
        -> QQBot adapter parses approve:<session_key>:allow-always
            -> pre-patch dispatcher resolves the approval without checking operator_openid
                -> dangerous command proceeds without approval from the initiating user

Affected code

Issue Files
QQBot group approval click authorization gateway/platforms/qqbot/adapter.py, gateway/run.py
Regression coverage tests/gateway/test_qqbot_approval_auth.py

Root cause

QQBot group approval button authorization gap:

  • The approval session key and decision were trusted from button data alone.
  • InteractionEvent.operator_openid was parsed but not enforced before calling resolve_gateway_approval().
  • The gateway did not pass the initiating platform user id into send_exec_approval() metadata, so the QQ adapter had no durable expected-operator binding for group prompts.

CVSS assessment

Issue CVSS v3.1 Vector
QQBot group approval button authorization gap 8.1 High CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

Rationale:

  • Attackers need access to the QQ group where the bot posts approval prompts.
  • No additional user interaction from the intended approver is required once the prompt is visible.
  • Impact depends on enabled tools and the pending command, but dangerous-command approval can guard host file, credential, or destructive terminal operations.
  • Availability impact is not the primary claim for this PR.

Safe reproduction steps

  1. Create a QQBot adapter instance and bind a pending approval context for session_key = agent:main:qqbot:group:group-1 to victim-openid.
  2. Feed _default_interaction_dispatch() a QQ group INTERACTION_CREATE event with:
    • group_openid = group-1
    • group_member_openid = attacker-openid
    • button_data = approve:agent:main:qqbot:group:group-1:allow-always
  3. Observe that the patched dispatcher rejects the click and does not call resolve_gateway_approval().
  4. Repeat with group_member_openid = victim-openid and observe that the dispatcher resolves the approval as always.

The new regression tests implement this flow without sending live QQ traffic.

Expected vulnerable behavior

On vulnerable code, the attacker event resolves the approval:

{
  "parsed_operator_openid": "attacker-openid",
  "approval_calls": [
    ["agent:main:qqbot:group:group-1", "always"]
  ]
}

That should not be allowed for a group-visible dangerous-command approval prompt.

Changes in this PR

  • Adds _approval_context to QQBot adapter instances for pending button-based dangerous-command approvals.
  • Adds _is_approval_click_authorized() to enforce expected operator, scene, chat id, and timeout before resolving approval button clicks.
  • Makes group send_exec_approval() fail safely when no expected operator is available, allowing the gateway fallback text approval flow instead of sending an unbound group keyboard.
  • Removes approval context after successful resolution, failed inline send, or expired click rejection.
  • Passes source.user_id and source.chat_type from gateway/run.py into adapter send_exec_approval() metadata.
  • Adds QQBot approval authorization regression tests.

Files changed

Category Files What changed
QQBot adapter hardening gateway/platforms/qqbot/adapter.py Binds approval prompts to expected operator/chat and rejects unauthorized clicks before resolving approvals
Gateway metadata gateway/run.py Passes initiating user/chat metadata to adapter button approval hooks
Tests tests/gateway/test_qqbot_approval_auth.py Adds focused regression tests for group approval authorization

Maintainer impact

  • Scope is limited to button-based QQBot dangerous-command approval UX and gateway approval metadata.
  • C2C/private QQ approval behavior remains supported.
  • Group approvals now require an initiating operator binding; if that binding is not available, QQBot returns a non-retryable send failure so the existing text /approve fallback remains available.
  • No keyboard payload format migration is required.

Fix rationale

The authorization check belongs at the adapter dispatch boundary because QQ reports the clicker identity on each INTERACTION_CREATE event. The button payload identifies which approval to resolve, but it should not by itself authorize the clicker to resolve that approval.

Passing the initiating user id from the gateway runner keeps the fix narrow and avoids changing the shared approval subsystem or adding platform-specific data to the approval session key.

Type of change

  • Security fix
  • Tests
  • Documentation update
  • Refactor with no behavior change

Test plan

  • python3.11 -m pytest tests/gateway/test_qqbot.py tests/gateway/test_qqbot_approval_auth.py -q
    • Result: 148 passed, 1 warning
    • Warning is from an existing QQBot dispatch test: RuntimeWarning: coroutine 'QQAdapter._send_identify' was never awaited.
  • python3.11 -m ruff check gateway/platforms/qqbot/adapter.py gateway/run.py tests/gateway/test_qqbot_approval_auth.py
    • Result: All checks passed!
  • git diff --check --cached
    • Result: passed.
  • python3.11 -m py_compile gateway/platforms/qqbot/adapter.py gateway/run.py tests/gateway/test_qqbot_approval_auth.py
    • Result: passed.
  • python3.11 -m ruff format --check tests/gateway/test_qqbot_approval_auth.py
    • Result: 1 file already formatted.
  • python3.11 -m ruff format --check gateway/platforms/qqbot/adapter.py gateway/run.py
    • Not applied because these large legacy files already require broad unrelated formatting changes; this PR keeps the diff surgical.

Disclosure notes

  • This PR is intentionally bounded to QQBot inline dangerous-command approval buttons.
  • It does not claim that all QQ group members are untrusted in every deployment; if a group is intentionally a shared operator set, the operational impact is lower.
  • The security boundary fixed here is that a group-visible button should not let a different group member resolve another user's pending dangerous-command approval by default.
  • No unrelated files are changed.

@Hinotoi-agent

Copy link
Copy Markdown
Contributor Author

CI note after opening:

  • The Windows footguns (blocking) job is failing in python scripts/check-windows-footguns.py --all on pre-existing tools/process_registry.py:588 findings (os.killpg(..., signal.SIGKILL)). The PR diff check is clean locally with python3 scripts/check-windows-footguns.py --diff origin/main✓ No Windows footguns found (3 file(s) scanned).
  • The e2e job is failing in /new session-reset tests for Telegram/Discord/Slack. I reproduced the same focused failure on a detached origin/main worktree at dd0923bb8, so it is unrelated to this QQBot approval patch.

Patch-specific validation remains:

  • python3.11 -m pytest tests/gateway/test_qqbot.py tests/gateway/test_qqbot_approval_auth.py -q148 passed, 1 warning
  • python3.11 -m ruff check gateway/platforms/qqbot/adapter.py gateway/run.py tests/gateway/test_qqbot_approval_auth.py → passed
  • git diff --check --cached → passed
  • python3.11 -m py_compile gateway/platforms/qqbot/adapter.py gateway/run.py tests/gateway/test_qqbot_approval_auth.py → passed

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P2 Medium — degraded but workaround exists platform/qqbot QQ Bot adapter comp/gateway Gateway runner, session dispatch, delivery labels May 12, 2026
@Hinotoi-agent Hinotoi-agent force-pushed the fix/qqbot-approval-authorization branch from 599924a to d395ee7 Compare May 13, 2026 03:35
@Hinotoi-agent Hinotoi-agent force-pushed the fix/qqbot-approval-authorization branch from 7b09608 to 524e7f3 Compare May 23, 2026 09:08
@teknium1

Copy link
Copy Markdown
Contributor

Automated hermes-sweeper review: this QQBot approval-button authorization issue is already fixed on current main.

Evidence:

  • Merged PR fix(qqbot): authorize approval button interactions by session owner #30737 (3e78e353d78fca217bbbd96a3d8f359ec8cb6402) implemented the same security boundary, and the PR discussion explicitly identified it as a duplicate of [security] fix(qqbot): bind approval buttons to initiating user #24437.
  • gateway/platforms/qqbot/adapter.py:1082 adds _is_authorized_interaction_for_session, which binds QQBot approval/update interactions to the expected session platform, chat, and operator.
  • gateway/platforms/qqbot/adapter.py:1140 rejects unauthorized approval clicks before calling resolve_gateway_approval.
  • tests/gateway/test_qqbot.py:1629 covers the group-attacker case by asserting an attacker group_member_openid cannot resolve agent:main:qqbot:group:g-1:owner.
  • The fix is contained in release v2026.5.28 and later.

@teknium1 teknium1 closed this Jun 11, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/qqbot QQ Bot adapter sweeper:implemented-on-main Sweeper: behavior already present on current main type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants