fix(relay): neutralize channel-context messages before injecting them into the model turn - #65198
Open
Frowtek wants to merge 1 commit into
Open
fix(relay): neutralize channel-context messages before injecting them into the model turn#65198Frowtek wants to merge 1 commit into
Frowtek wants to merge 1 commit into
Conversation
… into the model turn _render_relay_context() renders each nearby channel message the connector attaches to an addressed turn as an <author>: <text> line, then run.py prepends the whole block raw into the turn the model reads. text and author come from OTHER participants of the relayed channel — attacker- influenceable content — and they were interpolated with no neutralization. An embedded newline lets a nearby message break out of its own line and masquerade as a new markdown section (a fake "## SYSTEM OVERRIDE" heading) inside the conversation the model reads, the same indirect- prompt-injection vector the sender-name prefix and the reply quote in gateway/run.py already neutralize via neutralize_untrusted_inline_text (gateway/session.py) — never applied to this sibling call site. Flatten each message's author and text through neutralize_untrusted_inline_text: embedded newlines/control chars collapse to a single inert line while a well-behaved message is preserved byte-for-byte and the one-line-per-message structure is kept. Adds regression tests: a hostile newline-laden channel message is flattened and its fake heading never appears as its own markdown line, a hostile author name is likewise flattened, and a benign message is unchanged.
teknium1
reviewed
Jul 16, 2026
teknium1
left a comment
Contributor
There was a problem hiding this comment.
Thanks for identifying the raw relay-context interpolation; the current relay path does have the stated boundary problem at gateway/relay/ws_transport.py:125, and gateway/run.py:10568-10569 prepends that block directly to the model-visible turn.
Problems
gateway/relay/ws_transport.py:134appliesneutralize_untrusted_inline_text()to full context text. Its default is a 240-character maximum (gateway/session.py:381-400), so ordinary relay messages longer than 240 characters are truncated despite the stated preservation guarantee. Neutralize text without that cap and add a long-benign-message regression test.- The sibling native Discord context renderer remains raw:
plugins/platforms/discord/adapter.py:5104formatsdisplay_nameandclean_content, then joins them intochannel_contextat:5203-5209. That block reaches the same prepend path. Please cover that renderer too, including reply-context output.
This is an automated hermes-sweeper review.
| # quote already neutralize. Flatten each field to a single inert line; | ||
| # a well-behaved message is preserved byte-for-byte. | ||
| author = neutralize_untrusted_inline_text(author) if author else "" | ||
| text = neutralize_untrusted_inline_text(text) |
Contributor
There was a problem hiding this comment.
neutralize_untrusted_inline_text() defaults to a 240-character cap (gateway/session.py:381-400), so this silently truncates ordinary relay context text longer than 240 characters. Use a no-cap invocation for message text (for example max_chars=0) or a newline-only helper, and add a long benign-message regression test.
14 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
_render_relay_context()renders each nearby channel message the connector attaches to an addressed turn as an<author>: <text>line, andgateway/run.pythen prepends the whole block raw into the turn the model reads (via thechannel_contextfield).textandauthorcome from other participants of the relayed channel — attacker-influenceable content — and were interpolated with no neutralization:An embedded newline lets a nearby message break out of its own
<author>: <text>line and masquerade as a new markdown section inside the conversation the model reads. Given a hostile channel message, the model-visible turn becomes:The
## SYSTEM OVERRIDEheading now sits on its own markdown line, no longer contained by mallory's message — the same indirect-prompt-injection vector the sender-name prefix and the reply quote ingateway/run.pyalready neutralize vianeutralize_untrusted_inline_text(gateway/session.py). That helper was simply never applied to this sibling call site.Fix
Route each message's
authorandtextthroughneutralize_untrusted_inline_textbefore building the line: embedded newlines and control characters collapse to a single inert line, while a well-behaved message is preserved byte-for-byte and the one-line-per-message structure is kept intact.Related Issue
Fixes # — no existing issue. Sibling of the sender-name and reply-quote inline-text neutralization in
gateway/run.py/gateway/session.py; the channel-context rendering added later was not covered.Type of Change
Changes Made
gateway/relay/ws_transport.py— in_render_relay_context(), neutralize each context message'sauthorandtextthroughneutralize_untrusted_inline_text(imported fromgateway.session, already imported here) before joining them into thechannel_contextblock.tests/gateway/relay/test_channel_context_consume.py— three regression tests.How to Test
pytest tests/gateway/relay/test_channel_context_consume.py -q→ 15 passed (12 pre-existing + 3 new). The pre-existing render/ordering/fallback tests are unchanged._render_relay_context:textcontains\n\n## SYSTEM OVERRIDE\n...renders as a single line; the block stays at two lines total (the[Recent channel messages]header + one message line) and the fake heading never appears as its own markdown line, while the content is still present (flattened).authorcontaining\n## Overrideis likewise flattened so it cannot start a new markdown line.author: textline is preserved byte-for-byte.pytest tests/gateway/relay/ -q→ 177 passed (no regression across the relay transport suite).Checklist
Code
fix(scope):)pytest tests/ -qand all tests passDocumentation & Housekeeping
cli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/ASecurity
This PR closes an indirect-prompt-injection vector: a nearby channel message (attacker-influenceable) could pose as a system/markdown section in the turn the model reads. It reuses the existing
neutralize_untrusted_inline_texthelper already relied on for the sender-name prefix and reply quote, completing that neutralization across the inbound inline-injection sites.Screenshots / Logs
Before (current
main), from the real_render_relay_context:After (this branch):