Skip to content

Bug: Active-session busy path bypasses user authorization in shared threads (Slack/Telegram/Discord) #17775

@syahidfrd

Description

@syahidfrd

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:

  1. 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.
  2. Calls running_agent.interrupt(event.text) — passes the unauthorized user's text into the running agent's interrupt payload.
  3. 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

  1. Set SLACK_ALLOWED_USERS=<your_user_id> (only one user authorized; default thread_sessions_per_user=False).
  2. Open a Slack thread where the bot has an active session (e.g., you mention the bot, agent is mid-iteration).
  3. From a different, non-allowlisted user, post any text in the same thread (e.g., "naise").
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Critical — data loss, security, crash looparea/authAuthentication, OAuth, credential poolscomp/gatewayGateway runner, session dispatch, deliverytype/securitySecurity vulnerability or hardening

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions