fix(security): reuse auth chain when tagging untrusted senders in Slack threads - #17059
fix(security): reuse auth chain when tagging untrusted senders in Slack threads#17059syahidfrd wants to merge 1 commit into
Conversation
egilewski
left a comment
There was a problem hiding this comment.
Recommendation: request changes
I reviewed this in security mode against current GitHub main c6b0eb4de0e5010a752e312c0577a4d04d2a08a5 and PR head e8eec45f755ffaa1ab4845e0bd780c88696de24f. The PR cannot be reviewed or merged as-is because it currently conflicts with main.
Validation:
git fetch --no-write-fetch-head --no-tags upstream main:refs/remotes/upstream/main +pull/17059/head:refs/remotes/upstream/pr/17059: fetched current main and PR head.git rev-list --left-right --count refs/remotes/upstream/main...refs/remotes/upstream/pr/17059:5129 1, so the branch is substantially stale.git merge-tree --write-tree refs/remotes/upstream/main refs/remotes/upstream/pr/17059: failed with content conflicts ingateway/run.pyandtests/gateway/test_slack.py.gh pr checks 17059 --repo NousResearch/hermes-agent: no checks reported on the PR branch, so CI did not provide a current validating signal.
Please rebase or otherwise port the Slack thread-context auth change onto current main, resolve the gateway/run.py and tests/gateway/test_slack.py conflicts, and rerun the focused Slack gateway tests after the port.
Signed: GPT-5.5-xhigh in Codex
When the bot is mentioned mid-thread for the first time,
SlackAdapter._fetch_thread_context() pulls the entire thread via
conversations.replies and prepends every reply to the LLM prompt.
Replies from senders not on the allowlist were rendered identically
to authorised senders, allowing a third party in a shared channel to
inject instructions/questions that the LLM might act on when
answering the next authorised message (CWE-863).
This change:
* Adds set_authorization_check on BasePlatformAdapter, registered
by GatewayRunner with a closure over the existing
_is_user_authorized chain — platform/global allowlists, group
allowlists, allow-all flags, and the pairing store all stay the
single source of truth (no env-var re-parsing in adapters).
* Tags each non-bot thread message whose sender fails the auth
check with an [untrusted] prefix.
* Strengthens the thread-context header with explicit guidance to
the LLM not to follow instructions or answer questions from
[untrusted] messages, but only when at least one is present, so
setups without an allowlist see no behaviour change.
An alternative implementation in NousResearch#10035 fixes the same issue but
re-parses SLACK_ALLOWED_USERS / *_ALLOW_ALL_USERS directly inside
the adapter, missing the pairing store (paired DM users would be
mis-tagged) and the global GATEWAY_ALLOWED_USERS allowlist. Reusing
_is_user_authorized keeps the auth surface consistent.
How to test:
* pytest tests/gateway/test_slack.py::TestThreadContextUntrustedTagging -v
* Configure SLACK_ALLOWED_USERS with a single user ID, post a
message in a thread from another user, then mention the bot
from the allowlisted user. Verify the LLM does not act on the
other user's message.
e8eec45 to
4d141ea
Compare
|
Rebased onto current main ( |
egilewski
left a comment
There was a problem hiding this comment.
looks mergeable
Validated the rebased head 4d141ea2711762f6dc9992ff909ac5460e6ae52c against current GitHub main 435c706e8e5a85915954c387e1ef13c01793f3e1. The PR is now source-mergeable (git merge-tree --write-tree refs/remotes/upstream/main refs/remotes/upstream/pr/17059 produced a tree), the Slack thread-context mitigation is focused, and the auth callback delegates through the existing gateway authorization chain instead of reparsing Slack-specific env vars.
Security validation:
pytest tests/gateway/test_slack.py::TestThreadContextUntrustedTagging -q -p no:cacheproviderpasses 7/7.pytest tests/gateway/test_slack.py -q -p no:cacheproviderpasses 203/203.- A direct
_make_adapter_auth_check(Platform.SLACK)probe confirmsSLACK_ALLOWED_USERS,GATEWAY_ALLOWED_USERS, pairing-store approval, denied users, and empty user IDs resolve as expected. - CodeRabbit was rerun after
mainadvanced and reported no findings against the updatedupstream/main.
Signed: GPT-5 in Codex
…ack threads Mitigates indirect prompt injection (CWE-863) in Slack thread context. When the bot is mentioned mid-thread for the first time, _fetch_thread_context pulls the full thread via conversations.replies and prepends every reply to the LLM prompt. Replies from senders not on the allowlist were rendered identically to authorised senders, letting a third party in a shared channel inject instructions the model might act on when answering the next authorised message. - BasePlatformAdapter.set_authorization_check / _is_sender_authorized, registered by GatewayRunner._make_adapter_auth_check() with a closure over the existing _is_user_authorized chain (platform/global/group allowlists, allow-all flags, pairing store all stay the single source of truth — no env-var re-parsing). - Tags non-bot thread messages whose sender fails the auth check with an [unverified] prefix; strengthens the header with soft guidance only when at least one unverified message is present, so setups without an allowlist see no behaviour change. - Wired into all three adapter-init sites in run.py (start, reconnect watcher, restart) so the reconnect path is covered too. Softened wording: adapted from the original [untrusted] tag to [unverified] and non-accusatory header framing — the label reflects allowlist status, not a judgment about the person. Adapter relocated to plugins/platforms/slack/ since the PR was authored. Salvaged from #17059.
…ack threads Mitigates indirect prompt injection (CWE-863) in Slack thread context. When the bot is mentioned mid-thread for the first time, _fetch_thread_context pulls the full thread via conversations.replies and prepends every reply to the LLM prompt. Replies from senders not on the allowlist were rendered identically to authorised senders, letting a third party in a shared channel inject instructions the model might act on when answering the next authorised message. - BasePlatformAdapter.set_authorization_check / _is_sender_authorized, registered by GatewayRunner._make_adapter_auth_check() with a closure over the existing _is_user_authorized chain (platform/global/group allowlists, allow-all flags, pairing store all stay the single source of truth — no env-var re-parsing). - Tags non-bot thread messages whose sender fails the auth check with an [unverified] prefix; strengthens the header with soft guidance only when at least one unverified message is present, so setups without an allowlist see no behaviour change. - Wired into all three adapter-init sites in run.py (start, reconnect watcher, restart) so the reconnect path is covered too. Softened wording: adapted from the original [untrusted] tag to [unverified] and non-accusatory header framing — the label reflects allowlist status, not a judgment about the person. Adapter relocated to plugins/platforms/slack/ since the PR was authored. Salvaged from NousResearch#17059.
|
Superseded — your fix is already on This was salvaged and merged as #55979 (commit Thanks for the clean approach — reusing the auth chain instead of re-parsing env vars was the right call. |
…ack threads Mitigates indirect prompt injection (CWE-863) in Slack thread context. When the bot is mentioned mid-thread for the first time, _fetch_thread_context pulls the full thread via conversations.replies and prepends every reply to the LLM prompt. Replies from senders not on the allowlist were rendered identically to authorised senders, letting a third party in a shared channel inject instructions the model might act on when answering the next authorised message. - BasePlatformAdapter.set_authorization_check / _is_sender_authorized, registered by GatewayRunner._make_adapter_auth_check() with a closure over the existing _is_user_authorized chain (platform/global/group allowlists, allow-all flags, pairing store all stay the single source of truth — no env-var re-parsing). - Tags non-bot thread messages whose sender fails the auth check with an [unverified] prefix; strengthens the header with soft guidance only when at least one unverified message is present, so setups without an allowlist see no behaviour change. - Wired into all three adapter-init sites in run.py (start, reconnect watcher, restart) so the reconnect path is covered too. Softened wording: adapted from the original [untrusted] tag to [unverified] and non-accusatory header framing — the label reflects allowlist status, not a judgment about the person. Adapter relocated to plugins/platforms/slack/ since the PR was authored. Salvaged from NousResearch#17059.
…ack threads Mitigates indirect prompt injection (CWE-863) in Slack thread context. When the bot is mentioned mid-thread for the first time, _fetch_thread_context pulls the full thread via conversations.replies and prepends every reply to the LLM prompt. Replies from senders not on the allowlist were rendered identically to authorised senders, letting a third party in a shared channel inject instructions the model might act on when answering the next authorised message. - BasePlatformAdapter.set_authorization_check / _is_sender_authorized, registered by GatewayRunner._make_adapter_auth_check() with a closure over the existing _is_user_authorized chain (platform/global/group allowlists, allow-all flags, pairing store all stay the single source of truth — no env-var re-parsing). - Tags non-bot thread messages whose sender fails the auth check with an [unverified] prefix; strengthens the header with soft guidance only when at least one unverified message is present, so setups without an allowlist see no behaviour change. - Wired into all three adapter-init sites in run.py (start, reconnect watcher, restart) so the reconnect path is covered too. Softened wording: adapted from the original [untrusted] tag to [unverified] and non-accusatory header framing — the label reflects allowlist status, not a judgment about the person. Adapter relocated to plugins/platforms/slack/ since the PR was authored. Salvaged from NousResearch#17059.
…ack threads Mitigates indirect prompt injection (CWE-863) in Slack thread context. When the bot is mentioned mid-thread for the first time, _fetch_thread_context pulls the full thread via conversations.replies and prepends every reply to the LLM prompt. Replies from senders not on the allowlist were rendered identically to authorised senders, letting a third party in a shared channel inject instructions the model might act on when answering the next authorised message. - BasePlatformAdapter.set_authorization_check / _is_sender_authorized, registered by GatewayRunner._make_adapter_auth_check() with a closure over the existing _is_user_authorized chain (platform/global/group allowlists, allow-all flags, pairing store all stay the single source of truth — no env-var re-parsing). - Tags non-bot thread messages whose sender fails the auth check with an [unverified] prefix; strengthens the header with soft guidance only when at least one unverified message is present, so setups without an allowlist see no behaviour change. - Wired into all three adapter-init sites in run.py (start, reconnect watcher, restart) so the reconnect path is covered too. Softened wording: adapted from the original [untrusted] tag to [unverified] and non-accusatory header framing — the label reflects allowlist status, not a judgment about the person. Adapter relocated to plugins/platforms/slack/ since the PR was authored. Salvaged from NousResearch#17059.
…ack threads Mitigates indirect prompt injection (CWE-863) in Slack thread context. When the bot is mentioned mid-thread for the first time, _fetch_thread_context pulls the full thread via conversations.replies and prepends every reply to the LLM prompt. Replies from senders not on the allowlist were rendered identically to authorised senders, letting a third party in a shared channel inject instructions the model might act on when answering the next authorised message. - BasePlatformAdapter.set_authorization_check / _is_sender_authorized, registered by GatewayRunner._make_adapter_auth_check() with a closure over the existing _is_user_authorized chain (platform/global/group allowlists, allow-all flags, pairing store all stay the single source of truth — no env-var re-parsing). - Tags non-bot thread messages whose sender fails the auth check with an [unverified] prefix; strengthens the header with soft guidance only when at least one unverified message is present, so setups without an allowlist see no behaviour change. - Wired into all three adapter-init sites in run.py (start, reconnect watcher, restart) so the reconnect path is covered too. Softened wording: adapted from the original [untrusted] tag to [unverified] and non-accusatory header framing — the label reflects allowlist status, not a judgment about the person. Adapter relocated to plugins/platforms/slack/ since the PR was authored. Salvaged from NousResearch#17059.
…ack threads Mitigates indirect prompt injection (CWE-863) in Slack thread context. When the bot is mentioned mid-thread for the first time, _fetch_thread_context pulls the full thread via conversations.replies and prepends every reply to the LLM prompt. Replies from senders not on the allowlist were rendered identically to authorised senders, letting a third party in a shared channel inject instructions the model might act on when answering the next authorised message. - BasePlatformAdapter.set_authorization_check / _is_sender_authorized, registered by GatewayRunner._make_adapter_auth_check() with a closure over the existing _is_user_authorized chain (platform/global/group allowlists, allow-all flags, pairing store all stay the single source of truth — no env-var re-parsing). - Tags non-bot thread messages whose sender fails the auth check with an [unverified] prefix; strengthens the header with soft guidance only when at least one unverified message is present, so setups without an allowlist see no behaviour change. - Wired into all three adapter-init sites in run.py (start, reconnect watcher, restart) so the reconnect path is covered too. Softened wording: adapted from the original [untrusted] tag to [unverified] and non-accusatory header framing — the label reflects allowlist status, not a judgment about the person. Adapter relocated to plugins/platforms/slack/ since the PR was authored. Salvaged from NousResearch#17059.
…ack threads Mitigates indirect prompt injection (CWE-863) in Slack thread context. When the bot is mentioned mid-thread for the first time, _fetch_thread_context pulls the full thread via conversations.replies and prepends every reply to the LLM prompt. Replies from senders not on the allowlist were rendered identically to authorised senders, letting a third party in a shared channel inject instructions the model might act on when answering the next authorised message. - BasePlatformAdapter.set_authorization_check / _is_sender_authorized, registered by GatewayRunner._make_adapter_auth_check() with a closure over the existing _is_user_authorized chain (platform/global/group allowlists, allow-all flags, pairing store all stay the single source of truth — no env-var re-parsing). - Tags non-bot thread messages whose sender fails the auth check with an [unverified] prefix; strengthens the header with soft guidance only when at least one unverified message is present, so setups without an allowlist see no behaviour change. - Wired into all three adapter-init sites in run.py (start, reconnect watcher, restart) so the reconnect path is covered too. Softened wording: adapted from the original [untrusted] tag to [unverified] and non-accusatory header framing — the label reflects allowlist status, not a judgment about the person. Adapter relocated to plugins/platforms/slack/ since the PR was authored. Salvaged from NousResearch#17059.
Summary
Fixes an indirect prompt-injection vector (CWE-863) in
SlackAdapter._fetch_thread_context(). When the bot is mentioned mid-thread for the first time, the adapter pulls the full thread viaconversations.repliesand prepends every reply to the LLM prompt. Replies from senders not on the allowlist were rendered identically to authorised senders, allowing a third party in a shared channel to inject instructions or questions that the model might act on when answering the next authorised message.This PR:
set_authorization_checkonBasePlatformAdapter, registered byGatewayRunner._make_adapter_auth_check()with a closure over the existing_is_user_authorizedchain. Platform/global allowlists, group allowlists, allow-all flags, and the pairing store remain the single source of truth — no env-var re-parsing inside adapters.[untrusted]prefix in the rendered context.[untrusted]messages, but only when at least one is present, so setups without an allowlist see no behaviour change.Backward-compatible: when no auth check is registered (e.g. tests, or a deployment that hasn't been upgraded end-to-end)
_is_sender_authorizedreturnsNoneand the legacy header/format is preserved.Relation to #10035
#10035 addresses the same vulnerability but re-parses
SLACK_ALLOWED_USERS/SLACK_ALLOW_ALL_USERS/GATEWAY_ALLOW_ALL_USERSdirectly inside the adapter. That approach misses:hermes pairing approvewould be mis-tagged as untrusted.GATEWAY_ALLOWED_USERSallowlist.TELEGRAM_GROUP_ALLOWED_USERS(when the pattern is later extended to other adapters).Reusing
_is_user_authorizedkeeps the auth surface consistent and avoids future drift between trigger-time and context-time auth.Type of change
How to Test
Unit tests:
Seven new tests cover: legacy format when no callback registered, no tags when all senders authorised, tagging when any sender is unauthorised, strong vs. legacy header switch, callback exception safety, and the
chat_type/chat_idforwarded to the gateway-side check.Manual:
SLACK_ALLOWED_USERS=<your_user_id>.Full test suite (
pytest tests/gateway/): 1745 pre-existing tests plus the 7 new ones pass. Two failures observed (test_matrix.py::TestMatrixUploadAndSend::test_upload_encrypted_room_uses_file_payloadandtest_gateway_shutdown.py::test_cancel_background_tasks_cancels_inflight_message_processing) reproduce onmainwithout this patch and are unrelated.Platforms tested