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.81+ (pure Rust — no Python, no pyo3)
npm install disarm # Node.js 14+
gem install disarm # Ruby 3.1+
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? ~450M chars/sec on Latin (~38× Unidecode), ~106M on Cyrillic, ~712K slugs/sec (~10–24× python-slugify), ~65 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.81+ | disarm on crates.io |
guide · docs.rs |
| Ruby 3.1+, RubyGems 3.3.22+ | disarm on RubyGems |
guide |
| Node.js 14+ | disarm on npm |
guide |
| Java / 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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file disarm-0.16.0.tar.gz.
File metadata
- Download URL: disarm-0.16.0.tar.gz
- Upload date:
- Size: 886.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cb3313da5d14c481c650842a3af019043e84f3dcb89cfc01e3ad70415e51f146
|
|
| MD5 |
f4c6b646c9492f4a350257163876c9ea
|
|
| BLAKE2b-256 |
12185077e4b61db10d7c9c51332b5b13a69e428863982fc0c6bb678f9701419e
|
Provenance
The following attestation bundles were made for disarm-0.16.0.tar.gz:
Publisher:
publish.yml on raeq/disarm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
disarm-0.16.0.tar.gz -
Subject digest:
cb3313da5d14c481c650842a3af019043e84f3dcb89cfc01e3ad70415e51f146 - Sigstore transparency entry: 2743331236
- Sigstore integration time:
-
Permalink:
raeq/disarm@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Branch / Tag:
refs/tags/v0.16.0 - Owner: https://github.com/raeq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Trigger Event:
release
-
Statement type:
File details
Details for the file disarm-0.16.0-cp310-abi3-win_amd64.whl.
File metadata
- Download URL: disarm-0.16.0-cp310-abi3-win_amd64.whl
- Upload date:
- Size: 1.7 MB
- Tags: CPython 3.10+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5c475646c768125d6958c9aeef4066c9b11165d204fd5e0f843926278f9dd3d
|
|
| MD5 |
a98b9a455e4a97b3be1a5932fafa55d7
|
|
| BLAKE2b-256 |
fe79bd97f0d54aa42dbbeb3f8b7b89954ae2ed5c8aed1997facb8bfcd0935288
|
Provenance
The following attestation bundles were made for disarm-0.16.0-cp310-abi3-win_amd64.whl:
Publisher:
publish.yml on raeq/disarm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
disarm-0.16.0-cp310-abi3-win_amd64.whl -
Subject digest:
b5c475646c768125d6958c9aeef4066c9b11165d204fd5e0f843926278f9dd3d - Sigstore transparency entry: 2743331491
- Sigstore integration time:
-
Permalink:
raeq/disarm@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Branch / Tag:
refs/tags/v0.16.0 - Owner: https://github.com/raeq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Trigger Event:
release
-
Statement type:
File details
Details for the file disarm-0.16.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: disarm-0.16.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.8 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df11df12f1329216864dbd9b96ea45b30358d5ebadc6d24f7d43b5d4c62edc05
|
|
| MD5 |
fac07220082351fb90f98ad52ccd812e
|
|
| BLAKE2b-256 |
4ebc38e45d761ce225345a5a822fe130f94a3bbc857d9b0cce875e853a142b77
|
Provenance
The following attestation bundles were made for disarm-0.16.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
publish.yml on raeq/disarm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
disarm-0.16.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
df11df12f1329216864dbd9b96ea45b30358d5ebadc6d24f7d43b5d4c62edc05 - Sigstore transparency entry: 2743331354
- Sigstore integration time:
-
Permalink:
raeq/disarm@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Branch / Tag:
refs/tags/v0.16.0 - Owner: https://github.com/raeq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Trigger Event:
release
-
Statement type:
File details
Details for the file disarm-0.16.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: disarm-0.16.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 1.8 MB
- Tags: CPython 3.10+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
45ac368a379eb7c76b56200e37a1d1333cfbef01921fc46066f6ba77c967b341
|
|
| MD5 |
de0dbac6b048611f7dd62ff8eff92431
|
|
| BLAKE2b-256 |
43fd0cd44cafc92eedc7c6b7f35a5fef2ef2eb50219e54e5cab56d040077194a
|
Provenance
The following attestation bundles were made for disarm-0.16.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
publish.yml on raeq/disarm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
disarm-0.16.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
45ac368a379eb7c76b56200e37a1d1333cfbef01921fc46066f6ba77c967b341 - Sigstore transparency entry: 2743331413
- Sigstore integration time:
-
Permalink:
raeq/disarm@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Branch / Tag:
refs/tags/v0.16.0 - Owner: https://github.com/raeq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Trigger Event:
release
-
Statement type:
File details
Details for the file disarm-0.16.0-cp310-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: disarm-0.16.0-cp310-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.7 MB
- Tags: CPython 3.10+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a44764552a030128beaa03cc53632e669f0eb94d4eb328dcc0b31bd1f5b6d30
|
|
| MD5 |
21c393544c756811fac3ba907bd349c3
|
|
| BLAKE2b-256 |
8a236facf9039d54cfb3dee0f307607911304b0439087d3a4688fe0e13529ecd
|
Provenance
The following attestation bundles were made for disarm-0.16.0-cp310-abi3-macosx_11_0_arm64.whl:
Publisher:
publish.yml on raeq/disarm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
disarm-0.16.0-cp310-abi3-macosx_11_0_arm64.whl -
Subject digest:
6a44764552a030128beaa03cc53632e669f0eb94d4eb328dcc0b31bd1f5b6d30 - Sigstore transparency entry: 2743331533
- Sigstore integration time:
-
Permalink:
raeq/disarm@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Branch / Tag:
refs/tags/v0.16.0 - Owner: https://github.com/raeq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Trigger Event:
release
-
Statement type:
File details
Details for the file disarm-0.16.0-cp310-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: disarm-0.16.0-cp310-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 1.7 MB
- Tags: CPython 3.10+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
112e537dc924c75032ce7d83fcd3c1c138c6ba15d0bc5eaa62b5e1065b2873c9
|
|
| MD5 |
413d99d2bea83a05e5b577ebc70d3ce9
|
|
| BLAKE2b-256 |
5c4673dbc1a4eab9c72567131c40991ad464efa08d150e64c6b9a5c83ca9b127
|
Provenance
The following attestation bundles were made for disarm-0.16.0-cp310-abi3-macosx_10_12_x86_64.whl:
Publisher:
publish.yml on raeq/disarm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
disarm-0.16.0-cp310-abi3-macosx_10_12_x86_64.whl -
Subject digest:
112e537dc924c75032ce7d83fcd3c1c138c6ba15d0bc5eaa62b5e1065b2873c9 - Sigstore transparency entry: 2743331602
- Sigstore integration time:
-
Permalink:
raeq/disarm@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Branch / Tag:
refs/tags/v0.16.0 - Owner: https://github.com/raeq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2054bb84ff2c1f856a112b3672cf1194ee64fb1a -
Trigger Event:
release
-
Statement type: