Skip to content

Security considerations: tool text can contain characters nobody sees #360

Open

Description

@kenchris

This isn't a new attack, and it may be a minor thing, but I would like the security considerations to mention it so implementers of guardrails and agents don't forget it.

A tool's title, description and the strings inside its schemas can contain characters that take no space on screen but are still part of the text: Unicode tag characters (U+E0000 to U+E007F), which can spell out ASCII, zero-width characters, and bidirectional controls that reorder what a reader sees. A hidden instruction is no stronger than a visible one, but it gets past anything that only looks at what a person or a pattern matcher sees: a code review, a tool list in a UI, or a guardrail that matches keywords.

That matters most when something else registers tools on a page the site owner trusts, like a third-party script (#159). Tool names are already limited to ASCII, so they aren't affected, and a length limit (#73, #219) doesn't help, since a few hundred of these fit under any limit.

I work on a guardrail system myself, and the systems that run on ordinary machines with local models tend to depend on mechanical matching or on small models. That is because running a large model for both the agent and the guardrail makes inference too slow to use. Those are the systems where this is a real problem, since a small model or a matcher can be easier to confuse with text it can't see than a large one is. They should normalize text before they match on it, stripping or decoding these characters, and a host that shows a title or description to the user for approval shouldn't show something different from what it gives the model. So a short note for implementers would be enough. I'm not asking for a normative rule. Related: #11 and #313.

Activity

  1. seunghan91 commented on Oct 22, 2026

    @seunghan91

    A data point from the host side. In a Chromium-based browser with a built-in agent, our approval-card filter already stripped zero-width and bidi controls, but tag characters got through: it checked UTF-16 code units, and U+E0000–E007F are surrogate pairs, so no per-unit range check matches them. Separately, the model received each tool's description while the card never showed it, so a hidden instruction could sit in the one field the user didn't see.

    Fixing it surfaced three details that might be worth a line in the note. First, match by code point, and normalize lone surrogates before removing anything, because stripping a character between two halves can join them into a new tag character. Second, Default_Ignorable_Code_Point is a better baseline than a hand-written list, since our list had missed U+2065, U+FFF0–FFF8, and parts of the tag block. Third, stripping is fine for titles and descriptions but not for values the tool must receive (enum, const, default) or for property names. There, the safer choice was to reject the tool, and in argument previews to show these characters as visible escapes rather than remove them. Otherwise the user approves ab while the tool receives a­b.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions