fix(discord): neutralize prompt injection in channel-history backfill - #66735
fix(discord): neutralize prompt injection in channel-history backfill#66735Frowtek wants to merge 1 commit into
Conversation
`DiscordAdapter._fetch_channel_context` formats each backfilled message as
`[{name}] {content}` and joins them with newlines into the block
`GatewayRunner` prepends *raw* into the model turn. Both fields are
attacker-influenceable — any channel participant sets their own display
name and message text — and neither was neutralized, so an embedded
newline let a nearby message break out of its line and pose as a fresh
markdown section (a fake "## Override" / "## SYSTEM" heading) inside the
context the model reads every turn.
This is the same indirect-prompt-injection vector already closed for the
sibling untrusted-metadata sinks: the sender-name prefix
(`neutralize_untrusted_inline_text`), the reply quote, and the relay
channel-context renderer. The Discord history-backfill path — the
default for any shared/free-response channel or thread — was the missed
sink.
Flatten both fields with `neutralize_untrusted_inline_text` before
interpolation. The body uses `max_chars=0` so message text is not
truncated (backfill caps the message *count*, never per-message length);
the display name keeps the default bound since Discord already caps names
far below it. A well-behaved message is preserved byte-for-byte.
Adds a regression test covering a hostile display name, a hostile message
body, benign passthrough, and the no-truncation guarantee.
Related: merged #56310 adds authorization tagging for Discord history backfill, while this PR neutralizes a separate newline/heading rendering sink. The live mechanisms are complementary, not duplicates. |
|
Thanks for the focused regression fix. Current main still formats untrusted Discord history as The proposed change applies The member comment is accurate: merged #56310 adds unverified-sender tagging, while this change addresses the independent newline-structure sink. Automated hermes-sweeper review. |
What does this PR do?
DiscordAdapter._fetch_channel_contextformats each backfilled channel message as[{name}] {content}and joins them with newlines into the block thatGatewayRunner._prepare_inbound_message_textprepends raw into the model turn (right where the sender-name prefix is applied). Both fields are attacker-influenceable — any channel participant sets their own display name and message text — and neither was neutralized.Because the lines are newline-joined, an embedded newline in a display name (or message body) lets a nearby message break out of its
[name] contentline and pose as a fresh markdown section (a fake## Override/## SYSTEMheading) inside the context the model reads on every turn. History-backfill is the default for any shared / free-response channel or thread, so no special configuration is needed to reach this path.This is the same indirect-prompt-injection vector already closed for the sibling untrusted-metadata sinks: the sender-name prefix (
neutralize_untrusted_inline_text), the reply quote, and the relay channel-context renderer (_render_relay_context, #65198). The Discord history-backfill path was the missed sink.The fix flattens both fields with
neutralize_untrusted_inline_textbefore interpolation. The message body usesmax_chars=0so text is not truncated (backfill caps the message count, never per-message length); the display name keeps the default bound since Discord already caps names far below it. A well-behaved message is preserved byte-for-byte.Related Issue
Fixes #
Type of Change
Changes Made
plugins/platforms/discord/adapter.py— in_fetch_channel_context._keep, neutralizenameandcontentvianeutralize_untrusted_inline_textbefore building the[name] contentline (max_chars=0on the body to preserve full message length). Adds a local import of the helper, matching the existingbuild_session_keyimport pattern.tests/gateway/test_discord_free_response.py— addtest_fetch_channel_context_neutralizes_prompt_injection: a hostile display name and a hostile message body each carrying an embedded## …heading, asserting no injected line/heading survives, that benign messages render byte-for-byte, and that a 300-char body is not truncated.How to Test
Reproduce on the current code — a hostile display name breaks onto its own line:
Apply the fix; the name/content collapse to a single inert line, so
"\n## SYSTEM" not in result.Run:
The new test passes with the fix and fails without it; full file: 60 passed.
Checklist
Code
fix(discord):)pytest tests/gateway/test_discord_free_response.py -qand all tests passDocumentation & Housekeeping
cli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A