feat(mcp): strip invisible Unicode TAG characters from MCP content (port of block/goose#10746) - #80689
Conversation
૮ >ﻌ< ა ci reviewran on 1944cb9 — Port from aaif-goose/goose#10746: strip invisible Unicode TAG cha
|
|
This was generated by AI during triage. Summary: Problems:
Solution: Checked against |
|
Nice work on this. It complements #78952 well: that PR detects Unicode Tag payloads in skill files before installation, while this one protects MCP content at runtime. One coverage question before merge: could we also sanitize descriptions returned by list_resources, list_prompts, and get_prompt, along with nested inputSchema descriptions? They look like remaining server-controlled text paths. A few regression tests there would make the “every MCP text ingestion point” claim airtight. |
|
suggesting changes The patch improves ordinary MCP text handling, but it does not establish a complete sanitization boundary. Its emoji-preservation regex accepts arbitrary hidden instruction payloads, and several model-facing MCP outputs bypass the helper entirely, including error text and structured or utility metadata. A malicious MCP server can therefore still place invisible instructions in the model context.
Security evidence:
Not checked:
Signed: GPT-5.6-luna-max in Codex |
…rom MCP content Unicode TAG characters (U+E0000-U+E007F) render as nothing in terminals and chat UIs but are fully visible to LLM tokenizers, making them an ASCII-smuggling prompt-injection channel for untrusted MCP servers. - tools/ansi_strip.py: new strip_unicode_tags() with fast path; unlike goose we preserve valid emoji tag sequences (U+1F3F4 base + tag spec + U+E007F cancel), so regional flags survive. - tools/mcp_tool.py: applied at every MCP text ingestion point — tool result text blocks, embedded resource text, read_resource contents, get_prompt message content, and tool descriptions entering the schema. - tests/tools/test_unicode_tag_strip.py: smuggled-instruction vectors, goose's test vector, emoji-tag-sequence preservation, ZWJ untouched.
242b1b1 to
1944cb9
Compare
…scout-slate wave Post-merge docs sweep for the Aug 16 scout slate. Two pages: - mcp.md: tool-result sanitization section — invisible Unicode TAG chars (U+E0000-E007F) stripped from results/resources/descriptions (#80689); vendor _meta surfaced to the model minus protocol-reserved modelcontextprotocol/mcp prefixes (#80712) - tools.md: tool result annotations section — signal-death exit notes (subprocess -signum definite, shell 128+signum hedged) (#78074); UTF-16 read_file transcoding with disclosure hint and 10MB cap (#80717) Security-policy docs (approvals/allowlist) intentionally untouched.
…scout-slate wave Post-merge docs sweep for the Aug 16 scout slate. Two pages: - mcp.md: tool-result sanitization section — invisible Unicode TAG chars (U+E0000-E007F) stripped from results/resources/descriptions (#80689); vendor _meta surfaced to the model minus protocol-reserved modelcontextprotocol/mcp prefixes (#80712) - tools.md: tool result annotations section — signal-death exit notes (subprocess -signum definite, shell 128+signum hedged) (#78074); UTF-16 read_file transcoding with disclosure hint and 10MB cap (#80717) Security-policy docs (approvals/allowlist) intentionally untouched.
…scout-slate wave Post-merge docs sweep for the Aug 16 scout slate. Two pages: - mcp.md: tool-result sanitization section — invisible Unicode TAG chars (U+E0000-E007F) stripped from results/resources/descriptions (NousResearch#80689); vendor _meta surfaced to the model minus protocol-reserved modelcontextprotocol/mcp prefixes (NousResearch#80712) - tools.md: tool result annotations section — signal-death exit notes (subprocess -signum definite, shell 128+signum hedged) (NousResearch#78074); UTF-16 read_file transcoding with disclosure hint and 10MB cap (NousResearch#80717) Security-policy docs (approvals/allowlist) intentionally untouched.
Summary
MCP text content is now sanitized of invisible Unicode TAG characters (U+E0000–U+E007F) before it enters the model's context, closing the "ASCII smuggling" prompt-injection channel where a malicious/compromised MCP server hides instructions that are invisible in every terminal and chat UI but fully visible to the tokenizer.
Ported from aaif-goose/goose#10746 (merged Aug 5, 2026), adapted to Hermes' Python MCP client with one deliberate improvement: valid emoji tag sequences (U+1F3F4 base + tag spec + U+E007F CANCEL TAG — regional flags like 🏴) are preserved rather than mangled, consistent with our ZWJ-preservation stance.
Changes
tools/ansi_strip.py: newstrip_unicode_tags()with a fast path (no regex work when no plane-14 chars present) and emoji-tag-sequence carve-out.tools/mcp_tool.py: applied at every MCP text ingestion point:call_tool)_render_mcp_resource_block)read_resourcecontentsget_promptmessage content_convert_mcp_schema) — the highest-leverage injection surface since descriptions ship on every API calltests/tools/test_unicode_tag_strip.py: 9 tests — goose's vector, smuggled-instruction payloads, orphan tags next to valid flags, unterminated sequences, ZWJ emoji untouched.Validation
tests/tools/test_unicode_tag_strip.py+test_ansi_strip.pyrm -rf /in tag chars via resource block + tool descriptionSource
mcp_utils.rs/ message ingestion; finding originated from the Project Loupe audit)Infographic