Skip to content

fix(gateway): fail closed for approval-button auth on Slack, Feishu, Discord when no allowlist set#41226

Merged
teknium1 merged 4 commits into
mainfrom
security/approval-failopen-failclosed
Jun 7, 2026
Merged

fix(gateway): fail closed for approval-button auth on Slack, Feishu, Discord when no allowlist set#41226
teknium1 merged 4 commits into
mainfrom
security/approval-failopen-failclosed

Conversation

@teknium1

@teknium1 teknium1 commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Gateway approval-resolution authorization fails open on three messaging
adapters. When the agent posts a dangerous-command approval prompt to a chat
(Slack Block Kit buttons, a Feishu update-prompt card, a Discord component
button) and blocks until someone answers, the per-adapter caller check is
skipped entirely if no allowlist is configured — the default. Any member of
the channel who can see the prompt can click Approve and unblock the agent,
authorizing the dangerous command.

This is the cross-adapter variant of the Telegram fix in #24457 ("no allowlist
means deny by default"). Telegram and Matrix already fail closed; Slack,
Feishu (update-prompt path), and Discord did not.
This PR ports the
fail-closed fallback to all three.

It is distinct from approval-gate bypasses (which the threat model declines):
the classifier fires correctly and the prompt is posted as designed — the bug is
in who is permitted to answer it, which SECURITY.md lists as in scope, plus
the explicit line that a code path failing open with no allowlist is a bug in
scope.

What changed

  • Slack (gateway/platforms/slack.py) — new _is_interactive_user_authorized()
    helper, mirroring Telegram's _is_callback_user_authorized(): delegates to the
    gateway runner's central _is_user_authorized() when available (so
    GATEWAY_ALLOWED_USERS, DM pairing, and GATEWAY_ALLOW_ALL_USERS are all
    honored), then falls back to an env-only check that denies by default when
    neither SLACK_ALLOWED_USERS nor GATEWAY_ALLOWED_USERS is set. Both the
    approval-button and slash-confirm handlers now gate on it.
  • Feishu (gateway/platforms/feishu.py) — the update-prompt card path now
    runs through the same _allow_group_message() policy gate the approval path
    already uses (fail-closed under the default allowlist policy), plus
    same-chat binding so a callback from another chat can't answer a prompt.
  • Discord (plugins/platforms/discord/adapter.py) — _component_check_auth
    no longer returns True when both the user and role allowlists are empty.
    It now requires DISCORD_ALLOWED_USERS / DISCORD_ALLOWED_ROLES /
    GATEWAY_ALLOWED_USERS membership, or an explicit
    DISCORD_ALLOW_ALL_USERS / GATEWAY_ALLOW_ALL_USERS opt-in.

No capability regression

The fix is fail-closed-by-default with a recoverable opt-in, so deliberately-open
deployments keep working. Verified live (real handler/auth functions, not mocks),
both directions, for every adapter:

Configuration Before After
Empty allowlist, intruder clicks ✅ approved (BUG) ❌ denied
Operator in *_ALLOWED_USERS ✅ approved ✅ approved
User in central GATEWAY_ALLOWED_USERS ⚠️ ignored on Slack ✅ approved
GATEWAY_ALLOW_ALL_USERS=true (deliberate-open) ✅ approved ✅ approved
Feishu group_policy: open / admin ✅ approved ✅ approved

The only row whose behavior changes is the bug row (empty allowlist → now denies).

Test plan

pytest tests/gateway/test_slack_approval_buttons.py \
       tests/gateway/test_feishu_approval_buttons.py \
       tests/gateway/test_discord_component_auth.py \
       tests/gateway/test_discord_bot_auth_bypass.py \
       tests/gateway/test_slack.py
# 295 passed

Each adapter's regression tests pin: empty allowlist → deny; listed user → allow;
explicit allow-all opt-in → allow.

Salvage / attribution

This is a cluster salvage. Several contributors independently reported and fixed
parts of this bug; this PR consolidates the cleanest fix per adapter and
preserves their authorship (cherry-pick + per-commit author line):

Reported via coordinated disclosure by @Takyon236 (Takyon / reyse.ai —
attacks.ai "Glassware" engagement against hermes-agent, finding 13), and via
GitHub Security Advisories by @sfwani (GHSA-7cfc-9f2j-p78f, Slack;
GHSA-xmh3-35rm-p5pp, Matrix), @whyiug (GHSA-mc26-p6fw-7pp6, Discord), and
@Takyon236 (GHSA-xjrx-7xff-24qq). The original idea for click-time
authorization on Slack came from #6735 by @maymuneth; the Telegram fail-closed
precedent (#24457) was reported by @uwuziqobay25-lab. Thanks to all of them —
independent confirmation from multiple contributors is what got this prioritized.

Closes #33844
Closes #33866
Closes #30964

Co-authored-by: Dusk1e yusufalweshdemir@gmail.com
Co-authored-by: LaPhilosophie 804436395@qq.com

Infographic

approval-resolution fail-closed

Dusk1e and others added 4 commits June 7, 2026 05:03
… set

Salvage of the Discord half of PR #30964 by @LaPhilosophie. Discord
component button callbacks (ExecApprovalView, SlashConfirmView,
UpdatePromptView, ModelPickerView) bypass the normal message dispatch
authorization path. _component_check_auth previously returned True when
both the user and role allowlists were empty, so any guild member who
could see an approval prompt could click Approve on a dangerous command.

Fail closed instead: require DISCORD_ALLOWED_USERS / DISCORD_ALLOWED_ROLES
/ GATEWAY_ALLOWED_USERS membership, or an explicit DISCORD_ALLOW_ALL_USERS
/ GATEWAY_ALLOW_ALL_USERS opt-in for deliberately-open deployments.

Mirrors the Telegram (#24457) and Matrix fail-closed precedent.
The Slack half of #30964 is superseded by PR #33844's helper.

Reported via GHSA-mc26-p6fw-7pp6 (@whyiug).

Co-authored-by: LaPhilosophie <804436395@qq.com>
@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: security/approval-failopen-failclosed vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10002 on HEAD, 10001 on base (🆕 +1)

🆕 New issues (1):

Rule Count
invalid-argument-type 1
First entries
tests/gateway/test_discord_component_auth.py:134: [invalid-argument-type] invalid-argument-type: Argument to function `_component_check_auth` is incorrect: Expected `set[Unknown] | None`, found `list[int]`

✅ Fixed issues: none

Unchanged: 5189 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@liuhao1024

Copy link
Copy Markdown
Contributor

Positive verification — fail-closed approval-button auth is correct across Slack, Feishu, and Discord.

Reviewed the full diff (~760 lines across 3 adapters + tests).

  1. Discord fail-closed default is the key fix. The old _component_check_auth returned True when both allowed_user_ids and allowed_role_ids were empty ("no allowlist = allow everyone"). Any guild member could click approval buttons, cancel slash confirmations, or switch models — even if they'd be rejected at the slash/on_message gates. The new logic requires explicit DISCORD_ALLOW_ALL_USERS or GATEWAY_ALLOW_ALL_USERS to opt into permissive mode, otherwise falls through to deny. This closes a real privilege escalation surface.

  2. Slack _is_interactive_user_authorized has the correct fallback chain. Runner auth → SLACK_ALLOW_ALL_USERSSLACK_ALLOWED_USERS + GATEWAY_ALLOWED_USERSGATEWAY_ALLOW_ALL_USERS → deny. The _is_user_authorized delegation via __self__ on the bound method is a clean way to reuse the gateway's auth callback without duplicating allowlist parsing. The chat_type heuristic ("D" prefix → dm) is correct for Slack channel IDs.

  3. Feishu double-authorization + chat_id mismatch defense-in-depth. The _handle_update_prompt_card_action checks auth before calling _submit_on_loop, and _resolve_update_prompt re-checks auth after the async hop. The callback_chat_id != expected_chat_id guard prevents cross-chat callback forgery where an attacker crafts a card action from one chat to resolve a prompt in another. The get → validate → pop pattern in _resolve_update_prompt prevents TOCTOU races — two concurrent callbacks for the same prompt_id will both pass the auth check but only the first pop succeeds; the second returns early.

  4. Test coverage is thorough. test_discord_component_auth.py pins user/role/global allowlist semantics, explicit allow-all, and fail-closed behavior. test_slack_approval_buttons.py adds TestSlackInteractiveAuth (delegation to runner auth) and TestSlackSlashConfirmAction (global allowlist enforcement). test_feishu_update_prompt_auth.py covers chat_id mismatch and unauthorized click rejection.

  5. Dead code in Slack _handle_approval_action (allowed_csv = "" / if allowed_csv:) is harmless but could be cleaned up in a follow-up — the comment explains the intent.

No issues found. This is a well-scoped security fix that correctly aligns interactive component auth with the gateway's external-surface authorization model.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P1 High — major feature broken, no workaround comp/gateway Gateway runner, session dispatch, delivery platform/slack Slack app adapter platform/feishu Feishu / Lark adapter platform/discord Discord bot adapter labels Jun 7, 2026
@teknium1 teknium1 merged commit a317e54 into main Jun 7, 2026
21 of 23 checks passed
@teknium1 teknium1 deleted the security/approval-failopen-failclosed branch June 7, 2026 13:21
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 P1 High — major feature broken, no workaround platform/discord Discord bot adapter platform/feishu Feishu / Lark adapter platform/slack Slack app adapter type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants