Invisible Unicode Characters in Email: How to Find the Ghosts in Your Templates
Picture a Tuesday deploy. Marketing pastes the new welcome email copy from a shared doc into your template, you ship it, and everything looks perfect in the preview. Two days later a support ticket arrives: a customer's
Picture a Tuesday deploy. Marketing pastes the new welcome email copy from a shared doc into your template, you ship it, and everything looks perfect in the preview. Two days later a support ticket arrives: a customer's email client showed one line of the message in a strange order, and your keyword-based content filter in staging never flagged the word it was built to catch. You diff the copy. The diff is empty. Nothing is wrong. Except something is.
Welcome to invisible Unicode characters in email: characters that take up a position in your string, draw nothing (or rearrange what's around them), and survive every copy-paste with the stealth of a ninja who also happens to be a lawyer.
The two families of invisible characters
Most text never contains these, which is exactly why they surprise people.
| Family | Examples | What it does |
|---|---|---|
| Direction controls | Right-to-left override (U+202E), left-to-right embedding (U+202A), isolates (U+2066 to U+2069) | Tell the renderer to lay out the following characters in a different direction |
| Zero-width characters | Zero-width space (U+200B), word joiner (U+2060) | Occupy a position in the string but render as nothing |
Both have honest jobs in typesetting. The trouble starts when the text a reader sees and the text your code stores stop being the same string.
How they disguise a line
The classic demonstration is a file name. Put a right-to-left override in front of gpj.exe at the end of invoice_, and the name displays as invoice_exe.jpg. It looks like a picture. It's a program.
In 2021, security researchers showed the same trick making source code read one way to a reviewer and compile another way. That research, tracked as CVE-2021-42574, is why the technique is now widely called "Trojan Source".
Zero-width characters are quieter, but they cause similar headaches:
- Breaking up words. A zero-width space inside a word means a filter looking for that word no longer matches, while a human sees nothing unusual.
-
Making two strings look identical. A link label with an invisible character inside looks the same on screen as the real thing, but
===disagrees. - Tagging copies. Patterns of zero-width characters can watermark text so a leaked copy can be traced.
How they end up in ordinary email
Almost nobody puts them there on purpose. They arrive by paste: from a word processor, a shared doc, a web page, a chat message, or an old template that was itself pasted from somewhere else. Nothing on screen shows them, so they hitchhike from campaign to campaign like a burr on a sock.
If you build or send email from code, that means your templates, your CMS fields and your copy pipeline are all possible entry points.
Find them: a one-line regex
Most editors and every language with a regex engine can search for them. This pattern matches the direction controls and the common zero-width characters:
[\u200B\u2060\u202A-\u202E\u2066-\u2069]
A quick Node check over a template string:
const INVISIBLE = /[\u200B\u2060\u202A-\u202E\u2066-\u2069]/g;
function findInvisible(text) {
return [...text.matchAll(INVISIBLE)].map(m => ({
index: m.index,
codepoint: 'U+' + m[0].codePointAt(0).toString(16).toUpperCase().padStart(4, '0'),
}));
}
console.log(findInvisible('Welcome\u200B aboard'));
// [ { index: 7, codepoint: 'U+200B' } ]
A hit in text that's meant to be plain English is almost always safe to delete. Run it in CI against your email templates and you'll catch the next paste before a customer does.
Don't strip everything: the characters real languages need
Here's where the "just delete all of it" instinct goes wrong. Two invisible characters are load-bearing in real writing:
- Zero-width non-joiner (U+200C): Persian and Arabic spelling use it to stop letters connecting where they shouldn't.
- Zero-width joiner (U+200D): Indic scripts use it to form the right conjuncts.
That's why the regex above deliberately leaves out U+200C and U+200D. Strip them and you haven't cleaned your Persian newsletter, you've misspelled it.
And real right-to-left text (Arabic, Hebrew) doesn't need override characters at all. The standard HTML dir attribute on a block, with embedded left-to-right runs such as prices and URLs isolated, gets direction right without any invisible controls in the text.
What a sending platform can (and can't) do for you
This is the approach Email Digit takes, described in its guide to invisible Unicode characters in email: when an email is rendered for sending, the text of paragraphs, quotes and table cells is cleaned. Direction controls, the zero-width space, the word joiner and a few other invisible formatting codes such as the soft hyphen are removed, while the joiner and non-joiner are kept.
The guide is also upfront about the limits, which are worth copying into your own mental model whatever tool you use:
- Not every field is cleaned. Subject lines, headings, list items, button labels and raw HTML blocks aren't passed through it today, so check those yourself if the copy came from somewhere you don't trust. (Subject lines are the sneaky one; a subject line tester helps you look at them before sending.)
- Invisible isn't the same as lookalike. A URL that swaps a Latin "a" for a Cyrillic one is made of visible characters. No invisible-character filter will catch it. Read link destinations, not just link text.
- It isn't a spam filter. Cleaning hidden characters stops your own email carrying them by accident. It doesn't judge what the copy says.
A short checklist for your email pipeline
- Add the regex check to CI for every template and stored snippet.
- Normalize pasted copy at the input boundary (CMS field, admin form), not only at send time.
- Whitelist U+200C and U+200D so Persian, Arabic and Indic text survive.
- Use
dir="rtl"on blocks instead of override characters. - Hand-check subject lines and button labels, which many cleaners skip.
If a character is invisible and changes what a line means, it doesn't belong in an email. Your diff tool can't see it. Now your CI can.
I work on Email Digit, a deliverability-first email platform. Sources: Email Digit's guide on invisible Unicode characters (read November 12, 2026) and the Trojan Source CVE record.
This post was drafted with AI assistance and checked against the sources above.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.