hermes - ✅(Solved) Fix Bug: Active-session busy path bypasses user authorization in shared threads (Slack/Telegram/Discord) [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#17775Fetched 2026-05-01 05:55:56
View on GitHub
Comments
0
Participants
1
Timeline
11
Reactions
0
Author
Participants
Timeline (top)
labeled ×4referenced ×4cross-referenced ×2closed ×1

Error Message

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 ...

Root Cause

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.

Fix Action

Fix / Workaround

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.

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.

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).

PR fix notes

PR #17816: fix(gateway): enforce auth check in busy-session path to prevent unauthorized injection (#17775)

Description (problem / solution / changelog)

Summary

Adds the missing _is_user_authorized() gate at the top of _handle_active_session_busy_message(), closing an authorization bypass in shared-thread contexts.

Problem

When thread_sessions_per_user=False (the default), all participants in a Slack thread / Telegram forum topic / Discord thread share one session_key. The cold path (_handle_message) correctly calls _is_user_authorized() before creating a session, but the busy path — reached when an active session already exists — skipped this check entirely.

This allowed unauthorized users to:

  • Inject text into _pending_messages (queued as the next user turn)
  • Trigger agent.interrupt() with their content
  • Receive public acknowledgment ("⚡ Interrupting current task...")
  • Be directly addressed by the bot in subsequent responses

Confirmed in production (Slack, v2026.4.23).

Fix

A single authorization check at the entry point of _handle_active_session_busy_message(), before any queueing, steering, or interrupt logic:

if not self._is_user_authorized(event.source):
    logger.warning(...)
    return True  # silently dropped

This mirrors the cold-path gate and uses the same source of truth (per-platform allowlists, group allowlists, pairing store, allow-all flags).

Tests

Added tests/gateway/test_busy_session_auth_bypass.py with 4 focused test cases:

  • Unauthorized user dropped silently (no queue, no interrupt, no ack)
  • Authorized user still processed normally
  • Unauthorized user blocked even during drain mode
  • Unauthorized user cannot steer an active agent

All existing test_busy_session_ack.py tests (15) continue to pass.

Severity

P0 security — cross-user session contamination with confirmed real-world exploitation.

Closes #17775

Changed files

  • gateway/run.py (modified, +16/-0)
  • tests/gateway/test_busy_session_auth_bypass.py (added, +223/-0)

PR #17920: fix(gateway): enforce auth check in busy-session path to prevent unauthorized injection (#17775)

Description (problem / solution / changelog)

Salvage of #17816 by @Bartok9 onto current main.

Closes #17775.

Summary

Adds the missing _is_user_authorized() gate at the top of _handle_active_session_busy_message(), closing a P0 authorization bypass in shared-thread contexts (Slack/Telegram/Discord with thread_sessions_per_user=False, the default).

Root cause

The cold path (_handle_message) correctly calls _is_user_authorized() before creating a session, but the busy path — reached when an active session already exists — skipped the check entirely. Non-allowlisted users in the same thread as an authorized user could queue text into _pending_messages, trigger agent.interrupt() with their content, receive a public ⚡ Interrupting... ack, and end up addressed by name in the LLM reply.

Bypass commands (/stop, /new, /approve, etc.) were already safe — they route through _message_handler which hits the cold-path auth gate. The gap was exactly the busy/interrupt fallthrough.

Changes

  • gateway/run.py: 16-line auth gate at the top of _handle_active_session_busy_message — log warning, return True (handled = silently dropped).
  • tests/gateway/test_busy_session_auth_bypass.py: 4 new cases — unauthorized dropped, authorized still processed, unauthorized blocked during drain, unauthorized can't steer.

Validation

BeforeAfter
Intruder in shared threadqueued + interrupted + acked + addressed by namedropped silently
Authorized userprocessed normallyprocessed normally
Tests15/15 busy-ack pass19/19 pass (4 new + 15 existing)

Also ran all adjacent gateway auth suites: 41/41 pass across test_allowlist_startup_check, test_auth_fallback, test_discord_bot_auth_bypass, test_unauthorized_dm_behavior.

E2E reproduced the bypass on main and confirmed the fix blocks it — intruder → no queue, no interrupt, no ack; authorized user → unchanged.

Credit to @Bartok9 for the report-to-fix workflow and test coverage.

Changed files

  • gateway/run.py (modified, +16/-0)
  • tests/gateway/test_busy_session_auth_bypass.py (added, +223/-0)

Code Example

# 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 ...

---

# gateway/session.py:631
if source.thread_id and not thread_sessions_per_user:
    isolate_user = False

---

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 ...
RAW_BUFFERClick to expand / collapse

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

  • #16660 — also a session-isolation bug; ContextVars not propagated to tool worker threads, causing cross-session approval routing. Different surface, same theme.
  • #527 — RBAC tiers feature request; flags shared-session policy as an open question but does not file the underlying bypass as a bug.

extent analysis

TL;DR

Add an authorization check to the _handle_active_session_busy_message function to prevent unauthorized users from injecting content into an active agent session.

Guidance

  • Verify that the _is_user_authorized function is correctly implemented and working as expected in the cold path.
  • Add the suggested fix to the _handle_active_session_busy_message function to mirror the cold-path check and ensure that only authorized users can interact with an active session.
  • Test the fix in different shared-session contexts, such as Slack threads, Telegram forum topics, and Discord threads, to ensure that the authorization check is working correctly.
  • Consider implementing the alternative solution of gating the bypass-commands list and busy handler dispatch on authorization at the adapter level for additional security.

Example

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 ...

Notes

The suggested fix assumes that the _is_user_authorized function is correctly implemented and working as expected. If this function is not working correctly, additional debugging and testing will be required.

Recommendation

Apply the suggested fix to add an authorization check to the _handle_active_session_busy_message function. This will help prevent unauthorized users from injecting content into an active agent session and ensure that only authorized users can interact with the session.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

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.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix Bug: Active-session busy path bypasses user authorization in shared threads (Slack/Telegram/Discord) [2 pull requests, 1 participants]