fix(slack): stop interactive-caller auth falling open on multiplexed profiles - #72657
MilaArtyNew wants to merge 1 commit into
Conversation
…profiles
`SlackAdapter._is_interactive_user_authorized` gates approval-button,
slash-confirm and clarify clicks. Button clicks bypass the normal message
auth flow in `gateway/run.py`, so this is the only gate on that path.
It resolved the gateway auth chain by introspecting
`_message_handler.__self__`. On a multiplexed profile the handler is the
closure built by `GatewayRunner._make_profile_message_handler`, which has
no `__self__`, so the introspection silently yielded nothing and the method
fell through to its env-only fallback. That fallback opened with a raw
`os.getenv("SLACK_ALLOW_ALL_USERS")` read of the process environment — the
DEFAULT profile's `.env` — and its `_env()` helper fell through to
`os.getenv` on any scope miss.
A secondary profile therefore inherited another profile's allowlist and
allow-all flags, and the direction is fail-open: a caller that profile
never allowlisted could resolve its approvals. The profile-bound callback
the multiplexer already registers via `set_authorization_check`
(`gateway/run.py`) was never consulted.
Same introspection gap NousResearch#65589 describes for Telegram, where the fallback
happens to fail closed. On Slack it fails open.
- Prefer the injected `_is_sender_authorized` check, which delegates to the
full `_is_user_authorized` chain under this adapter's own profile. The
`__self__` introspection stays as the next step for adapters wired
without it (bare-adapter embedding, existing tests).
- Make the env fallback multiplex-safe: on a scope miss or
`UnscopedSecretError`, fail closed instead of reading `os.environ`.
Single-profile deployments keep the plain env read — there is no other
profile to leak from.
- Route the early `SLACK_ALLOW_ALL_USERS` check through the same scoped
helper, preserving its precedence over the allowlists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the focused Slack authorization fix. The premise is confirmed on current The proposed callback-first resolution uses the existing profile-bound callback configured for secondary adapters at No blocking issue found. The PR's adapter preimage still matches current main ( Automated hermes-sweeper review. |
|
suggesting changes The PR improves Slack's adapter-local fallback, but the production path now prefers GatewayRunner's injected authorization callback, whose shared _auth_env helper still falls through from an authoritative multiplex secret-scope miss to the default profile's process environment. A focused callback-chain probe reproduced the original allow-all cross-profile authorization on both current main and the reviewed head, including when the Slack socket task retains the secondary profile's secret scope. Consequently a user excluded by the secondary profile can still pass the interactive gate and reach approval, slash-confirm, or clarify resolution whenever the default profile enables SLACK_ALLOW_ALL_USERS or GATEWAY_ALLOW_ALL_USERS. The new tests substitute a hand-written callback and therefore do not exercise this production callback chain.
Security evidence:
Uncertainty: The Slack SDK and pytest development environment were not installed in the leased checkout, so no live Socket Mode event or repository pytest case was executed.; No network or GitHub state was consulted; review scope and revisions are those bound by the work order. Signed: GPT-5.6-sol-xhigh in Codex |
… profile check; gate reads never fall through to os.environ
`SlackAdapter._is_interactive_user_authorized` (approval / slash-confirm /
clarify Block Kit clicks) and the early pre-fetch gate in the message
handler recovered the runner via `_message_handler.__self__`, which is
None on a multiplexed adapter (closure handler) — so both fell to env-only
auth. The fallback read `SLACK_ALLOW_ALL_USERS` raw from `os.environ` and
its `_env` helper fell through to `os.environ` on a scoped miss: the
DEFAULT profile's allow-all flag / allowlist authorized callers on every
other profile's bot.
- Prefer the wired `set_authorization_check` callback (profile-bound
`_make_adapter_auth_check`) at both sites; keep `__self__` introspection
only for adapters wired without one.
- Env-only fallback reads go through `authz_mixin._platform_gate_env`
(scoped miss under multiplex → "", never os.environ); drop the raw
`os.getenv("SLACK_ALLOW_ALL_USERS")` pre-read.
Reapplies #72657 onto current main (original commit carried a bot
co-author trailer). Same class as Telegram #86296 / #65589.
Co-authored-by: MilaArtyNew <261982280+MilaArtyNew@users.noreply.github.com>
… profile check; gate reads never fall through to os.environ
`SlackAdapter._is_interactive_user_authorized` (approval / slash-confirm /
clarify Block Kit clicks) and the early pre-fetch gate in the message
handler recovered the runner via `_message_handler.__self__`, which is
None on a multiplexed adapter (closure handler) — so both fell to env-only
auth. The fallback read `SLACK_ALLOW_ALL_USERS` raw from `os.environ` and
its `_env` helper fell through to `os.environ` on a scoped miss: the
DEFAULT profile's allow-all flag / allowlist authorized callers on every
other profile's bot.
- Prefer the wired `set_authorization_check` callback (profile-bound
`_make_adapter_auth_check`) at both sites; keep `__self__` introspection
only for adapters wired without one.
- Env-only fallback reads go through `authz_mixin._platform_gate_env`
(scoped miss under multiplex → "", never os.environ); drop the raw
`os.getenv("SLACK_ALLOW_ALL_USERS")` pre-read.
Reapplies #72657 onto current main (original commit carried a bot
co-author trailer). Same class as Telegram #86296 / #65589.
Co-authored-by: MilaArtyNew <261982280+MilaArtyNew@users.noreply.github.com>
… profile check; gate reads never fall through to os.environ
`SlackAdapter._is_interactive_user_authorized` (approval / slash-confirm /
clarify Block Kit clicks) and the early pre-fetch gate in the message
handler recovered the runner via `_message_handler.__self__`, which is
None on a multiplexed adapter (closure handler) — so both fell to env-only
auth. The fallback read `SLACK_ALLOW_ALL_USERS` raw from `os.environ` and
its `_env` helper fell through to `os.environ` on a scoped miss: the
DEFAULT profile's allow-all flag / allowlist authorized callers on every
other profile's bot.
- Prefer the wired `set_authorization_check` callback (profile-bound
`_make_adapter_auth_check`) at both sites; keep `__self__` introspection
only for adapters wired without one.
- Env-only fallback reads go through `authz_mixin._platform_gate_env`
(scoped miss under multiplex → "", never os.environ); drop the raw
`os.getenv("SLACK_ALLOW_ALL_USERS")` pre-read.
Reapplies #72657 onto current main (original commit carried a bot
co-author trailer). Same class as Telegram #86296 / #65589.
Co-authored-by: MilaArtyNew <261982280+MilaArtyNew@users.noreply.github.com>
… profile check; gate reads never fall through to os.environ
`SlackAdapter._is_interactive_user_authorized` (approval / slash-confirm /
clarify Block Kit clicks) and the early pre-fetch gate in the message
handler recovered the runner via `_message_handler.__self__`, which is
None on a multiplexed adapter (closure handler) — so both fell to env-only
auth. The fallback read `SLACK_ALLOW_ALL_USERS` raw from `os.environ` and
its `_env` helper fell through to `os.environ` on a scoped miss: the
DEFAULT profile's allow-all flag / allowlist authorized callers on every
other profile's bot.
- Prefer the wired `set_authorization_check` callback (profile-bound
`_make_adapter_auth_check`) at both sites; keep `__self__` introspection
only for adapters wired without one.
- Env-only fallback reads go through `authz_mixin._platform_gate_env`
(scoped miss under multiplex → "", never os.environ); drop the raw
`os.getenv("SLACK_ALLOW_ALL_USERS")` pre-read.
Reapplies #72657 onto current main (original commit carried a bot
co-author trailer). Same class as Telegram #86296 / #65589.
Co-authored-by: MilaArtyNew <261982280+MilaArtyNew@users.noreply.github.com>
… profile check; gate reads never fall through to os.environ
`SlackAdapter._is_interactive_user_authorized` (approval / slash-confirm /
clarify Block Kit clicks) and the early pre-fetch gate in the message
handler recovered the runner via `_message_handler.__self__`, which is
None on a multiplexed adapter (closure handler) — so both fell to env-only
auth. The fallback read `SLACK_ALLOW_ALL_USERS` raw from `os.environ` and
its `_env` helper fell through to `os.environ` on a scoped miss: the
DEFAULT profile's allow-all flag / allowlist authorized callers on every
other profile's bot.
- Prefer the wired `set_authorization_check` callback (profile-bound
`_make_adapter_auth_check`) at both sites; keep `__self__` introspection
only for adapters wired without one.
- Env-only fallback reads go through `authz_mixin._platform_gate_env`
(scoped miss under multiplex → "", never os.environ); drop the raw
`os.getenv("SLACK_ALLOW_ALL_USERS")` pre-read.
Reapplies #72657 onto current main (original commit carried a bot
co-author trailer). Same class as Telegram #86296 / #65589.
Co-authored-by: MilaArtyNew <261982280+MilaArtyNew@users.noreply.github.com>
|
Thanks @MilaArtyNew for this PR — Merged via #101250 (11f932c) on current main. #101250 won because it was built on the earlier #65589 fix and covers the same behavior across all affected call paths in one change. You're credited via Closing this PR as superseded by the merged work. |
… profile check; gate reads never fall through to os.environ
`SlackAdapter._is_interactive_user_authorized` (approval / slash-confirm /
clarify Block Kit clicks) and the early pre-fetch gate in the message
handler recovered the runner via `_message_handler.__self__`, which is
None on a multiplexed adapter (closure handler) — so both fell to env-only
auth. The fallback read `SLACK_ALLOW_ALL_USERS` raw from `os.environ` and
its `_env` helper fell through to `os.environ` on a scoped miss: the
DEFAULT profile's allow-all flag / allowlist authorized callers on every
other profile's bot.
- Prefer the wired `set_authorization_check` callback (profile-bound
`_make_adapter_auth_check`) at both sites; keep `__self__` introspection
only for adapters wired without one.
- Env-only fallback reads go through `authz_mixin._platform_gate_env`
(scoped miss under multiplex → "", never os.environ); drop the raw
`os.getenv("SLACK_ALLOW_ALL_USERS")` pre-read.
Reapplies NousResearch#72657 onto current main (original commit carried a bot
co-author trailer). Same class as Telegram NousResearch#86296 / NousResearch#65589.
Co-authored-by: MilaArtyNew <261982280+MilaArtyNew@users.noreply.github.com>
What does this PR do?
Closes a fail-open in Slack interactive-caller authorization on multiplexed profiles.
SlackAdapter._is_interactive_user_authorizedgates approval-button, slash-confirm and clarify clicks; button clicks bypass the normal message auth flow ingateway/run.py, so this is the only gate on that path. On a secondary profile it stopped consulting the gateway auth chain entirely and authorized callers from the default profile's process environment instead.Bug Cause
Two defects compose:
The method resolved the gateway auth chain by introspecting
_message_handler.__self__. Undergateway.multiplex_profilesthe handler is the closure built byGatewayRunner._make_profile_message_handler, which has no__self__, so the introspection silently yielded nothing and control fell through to the env-only fallback. This is the same introspection gap fix(telegram): button-caller authorization breaks on multiplexed profiles (handler introspection) #65589 describes for Telegram.That fallback read the process environment — the default profile's
.env. Its first line was a rawos.getenv("SLACK_ALLOW_ALL_USERS"), and its_env()helper fell through toos.getenvwhenever the profile secret scope returned empty or raised.agent/secret_scope.pydeliberately refuses that read (returning the default under multiplex, raisingUnscopedSecretErrorwhen no scope is installed); the helper undid it.So a secondary profile inherited another profile's
SLACK_ALLOWED_USERS/GATEWAY_ALLOWED_USERSand, worse, itsSLACK_ALLOW_ALL_USERS/GATEWAY_ALLOW_ALL_USERSflags. On Telegram (#65589) this fallback happens to fail closed, so it is a denial bug. On Slack it fails open: a caller the profile never allowlisted can resolve its approvals.Meanwhile the correct authority was already available and unused — the multiplexer registers a profile-bound callback via
adapter.set_authorization_check(self._make_adapter_auth_check(platform, profile_name=profile_name)), which delegates to the full_is_user_authorizedchain (env allowlists, config allowlists, group allowlists, pairing store, allow-all flags) under that profile's own secret scope.This surface has been hardened before — #36848, #41226, #33844 — and the multiplex path reintroduced the same failure direction.
Reproduction Steps
gateway.multiplex_profileswith a default profile and a secondary profile running its own Slack app..envsetsSLACK_ALLOW_ALL_USERS=true(or simply lists its own users inSLACK_ALLOWED_USERS)..envsets only its bot token plusSLACK_ALLOWED_USERS=<its owner>.Expected: ignored,
[Slack] Unauthorized approval click by ….Before this PR: the click is honored.
Equivalent unit-level repro:
Fix
self._is_sender_authorized(...)check (registered for primary and multiplexed adapters alike) before any other resolution. ReturnsTrue/Falsewhen a check is wired,Nonewhen it isn't.__self__introspection as the next step, for adapters wired without the injected check (bare-adapter embedding, existing tests).UnscopedSecretError, fail closed rather than readingos.environ. Single-profile deployments keep the plain env read — there is no other profile to leak from.SLACK_ALLOW_ALL_USERScheck through the same scoped helper, preserving its existing precedence over the allowlists.No behavior change for single-profile deployments;
test_single_profile_env_fallback_unchangedandtest_handler_introspection_still_honored_without_injected_checkpin that.Related Issue
No issue filed for the Slack instance. Related: #65589 (same introspection gap on Telegram, fail-closed there), #70122 (the sibling
_auth_envfallthrough ingateway/authz_mixin.py, still open — this PR does not touch that file).Found via an adversarial scan of the external-surface authorization paths named in
SECURITY.md§2.6.Type of Change
Changes Made
plugins/platforms/slack/adapter.py— prefer the injected profile-bound auth check; multiplex-safe, fail-closed env fallbacktests/gateway/test_slack_interactive_authz_multiplex.py— new regression testsHow to Test
523 passed, 0 failed locally. 5 of the 8 new tests fail against
mainwithout the adapter change; the other 3 guard behavior this PR deliberately leaves alone.Checklist
Code
fix(scope):,feat(scope):, etc.)scripts/run_tests.shon relevant tests and they passDocumentation & Housekeeping
docs/, docstrings) — N/Acli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A