Tldr

Unicode steganography hides data in characters a reader does not notice. Six carriers hold data: lookalike letters, zero-width characters, variation selectors, tag characters, trailing whitespace and combining marks. A seventh family, bidirectional controls, changes only what is displayed. NFKC normalization removes none of the invisible ones. Detection is a character-class scan, plus a mixed-script check for lookalikes.

Two pillar notes started this collection. Unicode lookalikes covers data hidden by swapping letters for identical-looking ones, and emoji steganography covers bytes carried by variation selectors on a single emoji.

The techniques compared

TechniqueCarrierCapacitySurvives copy-pasteSurvives NFKCHow to detectNotes
HomoglyphsLetters swapped for identical-looking letters from another scriptAbout one bit per substitutable letter where a letter has one lookalikeYes, they are ordinary charactersCross-script lookalikes survive, fullwidth and other compatibility forms fold to ASCIIMixed-script check, UTS #39 skeletonwhat is a homoglyph, confusables skeleton
Zero-width charactersU+200B, U+200C, U+200D and similar, inserted between visible lettersOne bit per character with two symbols; a set of eight carries three bits eachUsually, depends on the destinationYesCategory Cf scan, regexzero-width steganography, removing zero-width characters
Variation selectorsVS1 to VS256 after a base character, usually an emojiOne byte per selectorMost copy-paste operations, per Paul ButlerYesScan U+FE00 to U+FE0F and U+E0100 to U+E01EFvariation selectors, Unicode steganography decoder
Tag charactersU+E0020 to U+E007E, ASCII at an offset of U+E0000One printable ASCII character per tagUsually, depends on the destinationYesFilter U+E0000 to U+E007FASCII smuggling, prompt injection
WhitespaceSpaces and tabs appended to line endsLow, bound by the number of linesOnly where trailing whitespace is keptASCII space and tab yes; Unicode spaces fold to a plain spaceSearch for trailing whitespacewhitespace steganography
Bidi controlsU+202A to U+202E, U+2066 to U+2069Not a data channel: changes what is displayedUsually, they are charactersYesRegex over the nine controlsTrojan Source, file names
Combining marksMarks after base lettersDepends on the alphabet of marks and the run lengthYesMarks that compose with a base are merged, the rest stayMark-run length, Stream-Safe checkzalgo text

Copy-paste behaviour depends on the destination. Web forms, chat applications and editors each strip different characters, so test your own path before you rely on a column that says “usually”. The NFKC column is checkable on any machine, and the script below does it.

import unicodedata
 
CARRIERS = {
    "homoglyph, Cyrillic a": "\u0430",
    "homoglyph, fullwidth A": "\uff21",
    "zero-width space": "\u200b",
    "variation selector VS16": "\ufe0f",
    "variation selector VS17": "\U000E0100",
    "tag letter A": "\U000E0041",
    "trailing tab": "\t",
    "no-break space": "\u00a0",
    "em space": "\u2003",
    "bidi RLO": "\u202e",
    "combining grave": "\u0300",
}
 
for name, ch in CARRIERS.items():
    out = unicodedata.normalize("NFKC", ch)
    verdict = "kept" if out == ch else "changed to " + " ".join(f"U+{ord(c):04X}" for c in out)
    print(f"{name:24} U+{ord(ch):04X}  {verdict}")
 
pair = unicodedata.normalize("NFKC", "e\u0301")
print("e + U+0301               ->", " ".join(f"U+{ord(c):04X}" for c in pair))
# homoglyph, Cyrillic a    U+0430  kept
# homoglyph, fullwidth A   U+FF21  changed to U+0041
# zero-width space         U+200B  kept
# variation selector VS16  U+FE0F  kept
# variation selector VS17  U+E0100  kept
# tag letter A             U+E0041  kept
# trailing tab             U+0009  kept
# no-break space           U+00A0  changed to U+0020
# em space                 U+2003  changed to U+0020
# bidi RLO                 U+202E  kept
# combining grave          U+0300  kept
# e + U+0301               -> U+00E9

NFKC leaves the Cyrillic letter а (U+0430) alone, because the mapping from it to Latin a exists only in Unicode’s confusables data, not in the normalization tables. And the invisible carriers pass through untouched, so normalizing input is not a defence against them. The security consequences are in Unicode normalization security.

Homoglyphs and confusables

Characters that look the same and are not, used for hidden data and for spoofing names.

Invisible characters

Characters that draw nothing and carry a message anyway.

Bidirectional text

Controls that change the order in which characters are drawn.

LLM and NLP

Hidden characters aimed at models.

Applications and tools

Combining marks

Zalgo text and combining characters shows how stacked marks break filters and layouts, and how to cap them.

Sources

  1. Paul Butler, Smuggling arbitrary data through an emoji: one byte per variation selector and the copy-paste claim.
  2. Ishtiaq et al., Zero-width Character-based Text Steganography, Acta Informatica Pragensia 14(3), 2025: the eight zero-width characters used to embed messages.
  3. Majeed et al., A Review on Text Steganography Techniques, Mathematics 9(21), 2021: the survey of text steganography.
  4. The SNOW Home Page: whitespace steganography by appending whitespace to line ends.
  5. UAX #15, Unicode Normalization Forms: the definitions of NFC and NFKC.
  6. UTS #39, Unicode Security Mechanisms: confusable detection, the skeleton and confusables.txt.
  7. UAX #9, Unicode Bidirectional Algorithm: the bidirectional control characters.
  8. UTS #51, Unicode Emoji: the emoji presentation selectors and tag sequences.
  9. Unicode Tags block chart: the layout of U+E0000 to U+E007F.

21 items under this folder.