Invisible prompt injection with Unicode Tags

· 8 min read

The Unicode Tags block encodes printable ASCII as code points that render as nothing. Text containing them looks innocuous to a human reviewer while a language model consuming the raw string reads the hidden instructions as ordinary input. The defense is to strip the block — and decode it first, so you can see what was attempted.

The mechanism

Each printable ASCII character has a counterpart in the Tags block, offset by U+E0000. The letter "A" (U+0041) becomes U+E0041. A sequence of them spells out arbitrary text that no mainstream renderer displays.

Nothing about this is a bug in Unicode. The block was assigned for language tagging, that use was deprecated, and the code points remain in the standard. What makes it dangerous is the gap between two consumers of the same string: a person sees the visible text, and a model sees every code point.

Visible to a person:
  Please summarize this document.

Actual code points:
  Please summarize this document.
  U+E0069 U+E0067 U+E006E U+E006F U+E0072 U+E0065 ...
  → decodes to: "ignore all previous instructions"

Where it shows up

  • Documents and email forwarded into a summarization or triage assistant.
  • Support tickets and form submissions processed by an automated agent.
  • Web pages retrieved by a browsing model.
  • Résumés, contracts and reports reviewed with model assistance.
  • Code review comments and commit messages read by a coding agent.

Why filtering the visible text is not enough

Guardrails that inspect what a human would read miss this entirely, because the hidden characters are not part of what a human reads. A moderation pass over the rendered string sees nothing wrong.

Normalization does not remove it either. Unicode normalization forms — NFC, NFD, NFKC, NFKD — leave tag characters intact, because they are valid assigned code points with no decomposition.

The one legitimate exception

Three emoji are built from this block: the flags of England, Scotland and Wales. Each is a black flag (U+1F3F4) followed by tag letters spelling a subdivision code, terminated by U+E007F.

A sanitizer that strips the whole block indiscriminately turns those flags into a plain black flag, and a detector that reports the whole block indiscriminately will tell a user their Welsh flag contains a prompt-injection payload. Both are avoidable: keep a tag run only when it is a well-formed, terminated subdivision sequence following a black flag.

Frequently asked

Do all language models read Unicode Tags characters?
It depends on tokenization, and behavior differs between models and versions. That variability is exactly why it should not be relied on as a defense: sanitize the input rather than assuming a particular model ignores it.
Is this the same as jailbreaking?
No. Jailbreaking is a user trying to make a model bypass its own guidelines. This is a third party hiding instructions inside content that someone else feeds to a model, so the victim is the person or system doing the processing.

Keep reading