fix(gateway): fail closed for approval-button auth on Slack, Feishu, Discord when no allowlist set#41226
Conversation
… 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>
🔎 Lint report:
|
| 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.
|
Positive verification — fail-closed approval-button auth is correct across Slack, Feishu, and Discord. Reviewed the full diff (~760 lines across 3 adapters + tests).
No issues found. This is a well-scoped security fix that correctly aligns interactive component auth with the gateway's external-surface authorization model. |
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
gateway/platforms/slack.py) — new_is_interactive_user_authorized()helper, mirroring Telegram's
_is_callback_user_authorized(): delegates to thegateway runner's central
_is_user_authorized()when available (soGATEWAY_ALLOWED_USERS, DM pairing, andGATEWAY_ALLOW_ALL_USERSare allhonored), then falls back to an env-only check that denies by default when
neither
SLACK_ALLOWED_USERSnorGATEWAY_ALLOWED_USERSis set. Both theapproval-button and slash-confirm handlers now gate on it.
gateway/platforms/feishu.py) — the update-prompt card path nowruns through the same
_allow_group_message()policy gate the approval pathalready uses (fail-closed under the default
allowlistpolicy), plussame-chat binding so a callback from another chat can't answer a prompt.
plugins/platforms/discord/adapter.py) —_component_check_authno longer returns
Truewhen both the user and role allowlists are empty.It now requires
DISCORD_ALLOWED_USERS/DISCORD_ALLOWED_ROLES/GATEWAY_ALLOWED_USERSmembership, or an explicitDISCORD_ALLOW_ALL_USERS/GATEWAY_ALLOW_ALL_USERSopt-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:
*_ALLOWED_USERSGATEWAY_ALLOWED_USERSGATEWAY_ALLOW_ALL_USERS=true(deliberate-open)group_policy: open/ adminThe only row whose behavior changes is the bug row (empty allowlist → now denies).
Test plan
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):
fix(slack): fail closed for interactive approval callbacks #33280 calls a nonexistent
build_source; fix(gateway): Slack approval handler must fail closed when SLACK_ALLOWED_USERS unset #36861 ignoresGATEWAY_ALLOWED_USERS).(its Slack half is superseded by fix(slack): re-check gateway auth on approval and slash-confirm buttons #33844's helper).
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