disarm

Invisible characters

Remove invisible characters from text

Paste text to strip zero-width spaces, bidi overrides, tag characters and the rest of the Unicode that renders as nothing. Cleaned text looks identical to what you pasted, so the tool tells you exactly what it took out.

The tool

0 chars
Cleaned0 chars

Most people have no hostile text to hand. The example hides a zero-width space inside a brand name, a right-to-left override, a byte order mark, a word joiner and a variation selector — seven characters that render as nothing. Copy it instead of loading it and you can watch them survive a trip through your clipboard into any other application.

Paste text to check it.

Nothing is uploaded. The engine runs inside this page.

Loading the engine…

A worked example

The tool above needs JavaScript. This is the same transformation, written out, so the result is legible without running anything.

Input, output and removed codepoints for the example
Input pay<U+200B>pal<U+200C><U+FEFF> ad<U+2060>min <U+202E>reversed<U+202C> text<U+FE0F><U+E0041>
Output paypal admin reversed text
Removed 8 codepoints across 4 classes — 4 zero-width, 2 bidi controls, 1 tag character, 1 variation selector

Rendered, the input reads as paypal admin reversed text already: the eight characters occupy no width. That is the difficulty. paypal contains a zero-width space between pay and pal, so it does not equal the string paypal and will not match a filter looking for it.

The same thing in your own code

Each block is a file CI compiles and runs, so none can quietly stop working, and all seven print the same line. disarm on GitHub →

# Remove every class of invisible character disarm exposes in all bindings.
#   pip install disarm
from disarm import (
    strip_zero_width_chars, strip_bidi, strip_tags,
    strip_variation_selectors, strip_noncharacters,
    strip_pua, strip_control_chars,
)

# The characters hidden in the sample below. Printing stripped text proves
# nothing — it looks the same either way — so assert they are gone instead.
HOSTILE = "​‌⁠‮‬️\U000e0041"

text = "pay​pal‌ ad⁠min ‮reversed‬ text️\U000e0041"

for step in (strip_zero_width_chars, strip_bidi, strip_tags,
             strip_variation_selectors, strip_noncharacters,
             strip_pua, strip_control_chars):
    text = step(text)

for ch in HOSTILE:
    assert ch not in text, f"U+{ord(ch):04X} survived"

print(f'ok: {len(HOSTILE)} hostile codepoints absent from "{text}"')

What counts as invisible

Most tools that do this carry a hand-written list of five or six codepoints. That catches the ones people remember and misses the rest, which is the problem: an attacker picks the ones people forget. disarm works by class, so the coverage does not depend on anyone's memory.

ClassCoversWhy it matters
Zero-width U+200B U+200C U+200D U+2060 U+FEFF Splits a word without showing a break, so pay​pal defeats an exact-match filter.
Bidi control U+00AD U+061C U+200E U+200F U+202A–U+202E U+2066–U+2069 Reorders rendered text against its stored order. The basis of Trojan Source (CVE-2021-42572).
Tag characters U+E0000–U+E007F An entire ASCII range with no visible form — the usual vehicle for hidden payloads in pasted text.
Variation selectors U+FE00–U+FE0F, U+E0100–U+E01EF Legitimate in emoji, and equally able to carry data no reader can see.
Private use U+E000–U+F8FF and planes 15–16 Renders differently or not at all depending on font. No agreed meaning to rely on.
Noncharacters U+FDD0–U+FDEF, U+xFFFE, U+xFFFF Permanently reserved as non-characters, yet still transmissible through most systems.
Other format Unicode category Cf The category-level sweep, so a format character nobody enumerated is still caught.
Control C0 and C1, excluding whitespace NUL, DEL and the C1 block. Tabs, newlines and carriage returns are deliberately kept.

Each row maps to a function you can call directly, and the report above names which one removed what. Nothing is classified twice: steps run most-specific first, so a zero-width space is reported as zero-width rather than as a generic format character.