disarm
Identify malicious attacks hiding in text.
раypal.com — Cyrillic а (U+0430) and р (U+0440) — renders identically to
paypal.com and is a different string. disarm finds that substitution and folds it back to
its Unicode TR39 prototype, strips bidi overrides,
zero-width and control characters, and flags spoofed hostnames — the Unicode layer your
validation, dedup, moderation and logging code is missing.
One pure-Rust core, with bindings for Python, Rust, Ruby, Node.js, Java/Kotlin and C.
from disarm import canonicalize, is_suspicious_hostname
# U+202E is a right-to-left override and U+200B a zero-width space. Neither is
# visible, and both survive a copy-paste straight into your database.
assert canonicalize("\u202eexample\u200b.com") == "example.com"
# U+0397 is Greek capital eta and U+13D4 Cherokee letter wa. They render as H and W.
assert canonicalize("\u0397ello \u13d4orld") == "Hello World"
# Cyrillic small a (U+0430) standing in for Latin a: renders as "apple.com".
suspicious, analysis = is_suspicious_hostname("\u0430pple.com")
assert suspicious and analysis.canonical == "apple.com"
Try it in your browser · Documentation · API reference
Install
pip install disarm # Python 3.10+ (wheels for Linux, macOS, Windows)
cargo add disarm # Rust 1.88+ (pure Rust — no Python, no pyo3)
npm install disarm # Node.js 22+
gem install disarm # Ruby 3.3+
disarm capabilities
- Confusable folding — TR39 visual mapping, plus what it misses
- Obfuscation stripping — bidi controls, zero-width characters, zalgo, emoji
- Hostname / IDN analysis — mixed-script, whole-script and bidi checks
- Ready-made pipelines —
canonicalize,catalog_key,search_key, LLM and RAG profiles - Transliteration — BGN/PCGN, ISO 9 and GOST; 83 language profiles
- Normalization, slugs & filenames — case folding, graphemes, encoding detection
from disarm import canonicalize, collapse_whitespace, slugify, strip_obfuscation, transliterate
# Cyrillic er (U+0440) and es (U+0441) folded to Latin p and c — visual (TR39) mapping.
assert strip_obfuscation("\u0440rodu\u0441t") == "product"
# No-break space (U+00A0), ideographic space (U+3000), thin space (U+2009) and a
# line separator (U+2028) all collapse to one plain ASCII space.
assert collapse_whitespace("Ada\u00a0\u3000Lovelace\u2009\u2028King") == "Ada Lovelace King"
# Their zero-width look-alikes are not whitespace at all — U+200B and U+FEFF are
# format characters, so neither str.split() nor collapse_whitespace touches them.
assert collapse_whitespace("A\u200bB\ufeffC") == "A\u200bB\ufeffC"
assert canonicalize("A\u200bB\ufeffC") == "ABC"
# Phonetic romanization: a different mapping, and not a defence.
assert transliterate("Київ", lang="uk") == "Kyiv"
assert slugify("Héllo Wörld") == "hello-world"
Performance & benchmarks
Does it work? On the XMR confusable-recovery metric, disarm's visual mapping scores
0.63–0.68, against ≤ 0.19 for phonetic transliterators (unidecode, anyascii,
uroman) and 0.10 for NFKC. →
the evidence ·
what it misses
What does it cost? 24–116× Unidecode on Latin-script text, ~13× on Cyrillic, Greek, Arabic, Persian and Hebrew, ~6–11× python-slugify, ~57 ns for an already-ASCII call. Hardware-dependent and directional, not guarantees. → full results · how to read them · where disarm is slower
Both come from "Fire Extinguishers Full of Gasoline": 435,864 observations over eight tools, six attack types, three tasks and two model architectures. Zenodo · CITATION.cff
Bindings: one core, six languages
Each binding reads like its own ecosystem — snake_case in Ruby, camelCase and .d.ts in
Node, builders in Java — over one shared core, so every language returns the same answer.
| Language | Package | Getting started |
|---|---|---|
| Python 3.10+ | disarm on PyPI |
guide |
| Rust 1.88+ | disarm on crates.io |
guide · docs.rs |
| Ruby 3.3+, RubyGems 3.3.22+ | disarm on RubyGems |
guide |
| Node.js 22+ | disarm on npm |
guide |
| Java 21+ / Kotlin | dev.disarm:disarm, dev.disarm:disarm-kotlin on Maven Central |
guide |
| C / other FFI | C ABI and disarm.h |
bindings/cabi |
Wheels, gems and addons are precompiled — no local Rust toolchain needed. The core crate is
unsafe_code = "forbid" and stays pure Rust; BINDINGS.md is the bar a new
binding has to meet.
Limitations: read this before deploying disarm
- Defense in depth, not a complete control. disarm folds the confusables it bundles and strips the format characters it enumerates. The confusable space is larger than any table, so measure your residue with
unmapped_confusables()rather than inferring it. Threat model.- Not an output sanitizer. disarm normalizes input. It performs no escaping —
<script>alert(1)</script>passes through unchanged, and NFKC can even surface ASCII metacharacters from fullwidth look-alikes. Keep encoding at the output sink (framework auto-escaping, DOMPurify, parameterized queries); run disarm before it.transliterate()is not a security control. It romanizes phonetically. For homoglyph defense usenormalize_confusables()/strip_obfuscation().
CONFUSABLES_VERSION reports which confusables.txt release the bundled tables were folded
from, so a deployment can answer "am I stale?" without inferring it from behaviour
(provenance).
Found a bypass? Report it under the security policy rather than in a public issue.
Links
| Documentation | https://docs.disarm.dev/ |
| Source code | https://github.com/raeq/disarm |
| PyPI package | https://pypi.org/project/disarm/ |
| Rust crate | https://crates.io/crates/disarm |
| Issue tracker | https://github.com/raeq/disarm/issues |
| Security policy | https://github.com/raeq/disarm/blob/main/SECURITY.md |
| Contributing | https://github.com/raeq/disarm/blob/main/CONTRIBUTING.md |
| Changelog | https://github.com/raeq/disarm/blob/main/CHANGELOG.md |
License
MIT
Metadata
Release files for disarm 0.17.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| disarm-0.17.2.tar.gz | 1.0 MB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| disarm-0.17.2-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| disarm-0.17.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| disarm-0.17.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| disarm-0.17.2-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
| disarm-0.17.2-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 10.5 MB
Release files / disarm-0.17.2.tar.gz
| Download URL | disarm-0.17.2.tar.gz |
|---|---|
| Size | 1.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f5775126e71fd7fb2ed9cdc4b1e888f6330f71d49bf471fac3380fdc7fd97a8b
|
|
BLAKE2b-256 checksum How to use checksums |
948f41fe18d8e05e29c3fde622c1300622b15009b97740e971ab892986f4b383
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / disarm-0.17.2-cp310-abi3-win_amd64.whl
| Download URL | disarm-0.17.2-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
93835bd9f5b4bdbc425e1ae4a8b902289d1d13bdb28a54791005b9f02ab2fc6c
|
|
BLAKE2b-256 checksum How to use checksums |
bcfe7bcf4ff79bef897a0711d849c3484b7455c1a253b543e89afd5f46374635
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / disarm-0.17.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | disarm-0.17.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 2.0 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
6fbe3668a2151d46e3e55a86e54330465d245c9116e3101037f9f2725557cea3
|
|
BLAKE2b-256 checksum How to use checksums |
4f54bc42406fd536af6cd3cf79a67bc05d0b45c9d2b117f0b8ed695873d0adfe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / disarm-0.17.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | disarm-0.17.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
0b4c7257d4a1dcd5b44a51668bbaf5c3b1b0aac1f8cee3cbc4a445936c7db398
|
|
BLAKE2b-256 checksum How to use checksums |
497eef0ac3438e8673b1ac4448a5c9dceb970b6bdf2fc4835ec63499b5825097
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / disarm-0.17.2-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | disarm-0.17.2-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.8 MB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
893996ed03c41f2a7af1cbed9cc3eeb721e4c25dbbf34607635fe3b394ba6eb7
|
|
BLAKE2b-256 checksum How to use checksums |
a00469d4123bb69916cb78cd7d1b60885fa3847f29e33cff1154aaee0abb9313
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / disarm-0.17.2-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | disarm-0.17.2-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
a902a8b7d18c503e41771148ba79ab73da75b5ee72c32932a7b6fd4f6643f0f9
|
|
BLAKE2b-256 checksum How to use checksums |
0592666c685b0ed7f24dff719cde60b09fa0f59fc3d927f2413f291fbd76feec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency log