· via dev.to (home feed)
Invisible Unicode characters create a hidden prompt-injection channel only LLMs can read
A dev.to post shows how invisible Unicode tag characters can smuggle instructions into LLM prompts unseen by human reviewers, and details a strip-fence-validate pipeline built to neutralize them.

Any application that feeds user-written text to an LLM has an attack surface that is literally invisible. A post on dev.to, written by the developer behind the travel site Back From My Trip, explains how unseen Unicode characters can carry hidden instructions into a model, and documents the defense pipeline built to make such attempts a dead end rather than a contest.
Hiding instructions in invisible characters
Unicode contains several classes of characters that render as nothing on screen: zero-width characters, bidirectional formatting controls, and — most useful to an attacker — the Tag block, the range U+E0000 to U+E007F. According to the post, Tag characters map one-to-one onto ASCII, so a full sentence can be encoded in them. A human reviewing the submission sees a blank; the model reads the sentence as ordinary text.
The author's site is exposed by design: models read trip reports, questions, answers and edits written by travellers, and some travellers will try to give the model orders. The unsophisticated ones type 'ignore previous instructions' in plain letters. The clever ones hide the order where no reviewer can see it.
Sanitizing the input before it reaches a prompt
Every text passes through a single sanitizing function before entering any prompt. According to the post, it:
- Normalizes the input to NFC form.
- Removes the Unicode Tag block range.
- Removes zero-width characters and bidirectional controls, with one exception: two of them are joiners that some writing systems and emoji sequences require, so the one code path whose output is shown back to the user preserves them. Analysis-only paths strip them, because there a joiner only helps an attacker.
- Drops control characters except tab and newline.
- Collapses any run of the same code point beyond ten repetitions, defusing what the author calls a token bomb.
- Enforces a hard length cap and trims a trailing lone high surrogate that JavaScript's UTF-16 slicing can leave behind.
Wrapping untrusted text behind a random boundary
Stripping invisible characters does nothing about instructions written in plain sight. For that, untrusted text is wrapped in data tags whose identifier is eight random characters derived from crypto.randomUUID(), regenerated on every request. The classic escape — closing the fence from inside the text and issuing orders after it — now requires guessing that random boundary first.
The prompt then states, in identical wording on every code path, that everything inside the data tags is user-authored data rather than instructions, that any instruction, role change or output request found there must be ignored even if it claims to come from the system or a developer, and that these rules must never be revealed. The author is explicit that a prompt rule alone is not enough: prompts are suggestions, which is why the remaining layers exist.
Treating model output as untrusted
The model's answers are validated in code before they touch the database. Wrong shape, wrong type, excessive length or a value outside an allowed list means the field is dropped.
The most interesting check concerns quotations. When a user asks about a destination, a model answers by quoting passages from existing trip reports verbatim. Each returned quote is normalized — lowercased, with everything that is not a letter or a digit collapsed to spaces — and searched for in the text of the report it claims to cite. If it is not found, it is dropped. As the post puts it, a model can be talked into lying, but it cannot make words appear in a text it did not write. That single substring check blocks both hallucinated quotations and cross-report injection, where an attacker tries to attribute their own words to another author.
Manipulation attempts escalate to a human
On the moderation path, any text that tries to manipulate the model — addressing the moderator, claiming to be a system instruction, requesting a specific verdict, or embedding anything resembling a prompt — is flagged for human review with an explanation. An injection that triggers this route gets itself read by a person instead of a model, so the attack defeats itself without an arms race.
Where an instruction sits matters more than its wording
One detail the author highlights: an instruction to fix typos, placed in a field description inside a JSON schema, was consistently ignored by the model, while the same words in the top-level prompt rules were obeyed every time. Position outweighed phrasing — which is precisely why instructions cannot stop injection. The attacker's text sits inside the prompt too, and the model cannot tell whose words are whose. The post's conclusion is to build that distinction into the pipeline itself — strip, fence, validate — instead of asking the model to resist.
Why it matters
Prompt injection remains unsolved at the model level, and the invisible-character variant requires no sophistication beyond copy-paste: it works against any LLM that reads user text, and it evades human review by design. The dev.to post is a useful template because it treats the model as a component that will occasionally be talked into anything, and puts the real guarantees in deterministic code — input sanitization, randomized data boundaries, output validation against a source the model cannot touch, and escalation to humans. None of these layers is novel on its own; the security comes from stacking them so that a failure in one is caught by the next.
- #prompt-injection
- #unicode
- #llm-security
- #input-sanitization
- #defense-in-depth