Bug Description
In shared-session contexts (Slack threads, Telegram forum topics, Discord threads — i.e. when thread_sessions_per_user=False, the default), unauthorized users can inject content into an active agent session. The auth gate at gateway/run.py:3452 (_is_user_authorized) is bypassed whenever there is already an active session for the same session_key, because the adapter's handle_message short-circuits to a busy/interrupt handler that does not re-check authorization.
This means a non-allowlisted user in the same Slack thread as an authorized user can:
- interrupt the authorized user's running task,
- have their text inserted into the next-turn LLM context as a user message,
- be addressed directly by the bot in its reply.
Real-world reproduction
In a deployment with SLACK_ALLOWED_USERS=<syahid_id> only (only one user authorized):
- Syahid (allowlisted) is mid-task with the bot in a Slack thread.
- Cholis (NOT allowlisted) posts "naise" in the same thread.
- The bot interrupts the in-flight task ("⚡ Interrupting current task. I'll respond to your message shortly.") and replies with (translated from Indonesian): "Haha sorry Cholis, got carried away with 'looks-clean-in-Markdown' formatting but it's unreadable on Slack mobile 😅 Next response I'll use bullet/section format instead, no tables."
The bot only knows Cholis's identity if her message reached the LLM context. She is not on the allowlist, so this is a confirmed auth-gate bypass.
Mechanism
The auth check (_is_user_authorized in gateway/run.py:3452-3485) is invoked from GatewayRunner._handle_message, but only on the cold path — when no active session exists for the session_key.
When an active session exists, gateway/platforms/base.py:2212-2282 takes a different branch:
# gateway/platforms/base.py:2212
if session_key in self._active_sessions:
# ... bypass-command handling for /stop, /new, /approve, etc. ...
if self._busy_session_handler is not None: # line 2262
try:
if await self._busy_session_handler(event, session_key): # ← NO AUTH CHECK
return
except Exception as e:
logger.error(...)
# ... merges event into _pending_messages, signals interrupt ...
The busy handler (GatewayRunner._handle_active_session_busy_message at gateway/run.py:1651-1804) does not call _is_user_authorized. It:
- Calls
merge_pending_message_event(adapter._pending_messages, session_key, event) — the unauthorized user's event becomes the next-turn user message after the current run finishes or is interrupted.
- Calls
running_agent.interrupt(event.text) — passes the unauthorized user's text into the running agent's interrupt payload.
- Sends a public "⚡ Interrupting current task" acknowledgment back to the channel.
Why threads amplify this
build_session_key at gateway/session.py:572-637 defaults to shared session keys for threads (thread_sessions_per_user=False):
# gateway/session.py:631
if source.thread_id and not thread_sessions_per_user:
isolate_user = False
All participants in a Slack thread share one session_key. So a non-allowlisted user posting in the same thread as an allowlisted user always hits _active_sessions[K] and routes to the busy path — same as the allowlisted user — and bypasses the auth gate that would otherwise drop them on the cold path.
Steps to reproduce
- Set
SLACK_ALLOWED_USERS=<your_user_id> (only one user authorized; default thread_sessions_per_user=False).
- Open a Slack thread where the bot has an active session (e.g., you mention the bot, agent is mid-iteration).
- From a different, non-allowlisted user, post any text in the same thread (e.g., "naise").
- Observe:
- The bot posts "⚡ Interrupting current task..." to the thread.
- When the run resumes, the unauthorized user's text appears in the LLM context as a user message.
- The bot may directly address the unauthorized user by name or react to their content.
Expected behavior
Messages from non-allowlisted users should be dropped silently in the busy path, matching the cold-path behavior at gateway/run.py:3452-3485 (which logs a warning and returns None). No interrupt should fire, nothing should be merged into _pending_messages, and no public acknowledgment should be sent.
Suggested fix
Add the same _is_user_authorized check at the top of _handle_active_session_busy_message:
async def _handle_active_session_busy_message(self, event: MessageEvent, session_key: str) -> bool:
if not self._is_user_authorized(event.source):
logger.warning(
"Unauthorized user in active session: %s (%s) on %s — dropping",
event.source.user_id, event.source.user_name, event.source.platform.value,
)
return True # message handled (dropped); do not fall through to default path
# ... existing logic ...
This keeps the gateway-side auth chain (per-platform allowlists, group allowlists, pairing store, allow-all flags) as the single source of truth, mirroring the cold-path check.
Alternatively, the bypass-commands list at gateway/platforms/base.py:2226 (should_bypass_active_session) and the busy handler dispatch at line 2262 could both gate on authorization at the adapter level, but the gateway-side check is cheaper to maintain.
Severity
P2 — comparable to #16660 in attack surface (cross-user session contamination), with confirmed real-world exploitation in shared Slack channels. The only workaround is thread_sessions_per_user=True, which fundamentally changes thread UX (each user gets a separate session, breaking shared-thread collaboration that is the typical Slack thread pattern).
This affects every adapter that supports shared-session contexts:
- Slack threads (confirmed)
- Telegram forum topics
- Discord threads
- Any future platform with thread-shared sessions
Environment
v2026.4.23 (bf196a3f)
- Python 3.11.15, macOS Darwin 24.6
- Slack adapter, shared-thread session, default
thread_sessions_per_user=False
Related
Bug Description
In shared-session contexts (Slack threads, Telegram forum topics, Discord threads — i.e. when
thread_sessions_per_user=False, the default), unauthorized users can inject content into an active agent session. The auth gate atgateway/run.py:3452(_is_user_authorized) is bypassed whenever there is already an active session for the samesession_key, because the adapter'shandle_messageshort-circuits to a busy/interrupt handler that does not re-check authorization.This means a non-allowlisted user in the same Slack thread as an authorized user can:
Real-world reproduction
In a deployment with
SLACK_ALLOWED_USERS=<syahid_id>only (only one user authorized):The bot only knows Cholis's identity if her message reached the LLM context. She is not on the allowlist, so this is a confirmed auth-gate bypass.
Mechanism
The auth check (
_is_user_authorizedingateway/run.py:3452-3485) is invoked fromGatewayRunner._handle_message, but only on the cold path — when no active session exists for thesession_key.When an active session exists,
gateway/platforms/base.py:2212-2282takes a different branch:The busy handler (
GatewayRunner._handle_active_session_busy_messageatgateway/run.py:1651-1804) does not call_is_user_authorized. It:merge_pending_message_event(adapter._pending_messages, session_key, event)— the unauthorized user's event becomes the next-turn user message after the current run finishes or is interrupted.running_agent.interrupt(event.text)— passes the unauthorized user's text into the running agent's interrupt payload.Why threads amplify this
build_session_keyatgateway/session.py:572-637defaults to shared session keys for threads (thread_sessions_per_user=False):All participants in a Slack thread share one
session_key. So a non-allowlisted user posting in the same thread as an allowlisted user always hits_active_sessions[K]and routes to the busy path — same as the allowlisted user — and bypasses the auth gate that would otherwise drop them on the cold path.Steps to reproduce
SLACK_ALLOWED_USERS=<your_user_id>(only one user authorized; defaultthread_sessions_per_user=False).Expected behavior
Messages from non-allowlisted users should be dropped silently in the busy path, matching the cold-path behavior at
gateway/run.py:3452-3485(which logs a warning and returnsNone). No interrupt should fire, nothing should be merged into_pending_messages, and no public acknowledgment should be sent.Suggested fix
Add the same
_is_user_authorizedcheck at the top of_handle_active_session_busy_message:This keeps the gateway-side auth chain (per-platform allowlists, group allowlists, pairing store, allow-all flags) as the single source of truth, mirroring the cold-path check.
Alternatively, the bypass-commands list at
gateway/platforms/base.py:2226(should_bypass_active_session) and the busy handler dispatch at line 2262 could both gate on authorization at the adapter level, but the gateway-side check is cheaper to maintain.Severity
P2 — comparable to #16660 in attack surface (cross-user session contamination), with confirmed real-world exploitation in shared Slack channels. The only workaround is
thread_sessions_per_user=True, which fundamentally changes thread UX (each user gets a separate session, breaking shared-thread collaboration that is the typical Slack thread pattern).This affects every adapter that supports shared-session contexts:
Environment
v2026.4.23(bf196a3f)thread_sessions_per_user=FalseRelated