ipfy / homoglyph
Detect visually confusable domain names using Unicode UTS #39.
README
Unicode-aware detection of visually confusable domain names.
Homoglyph is a multi-language, standards-based library for spotting Unicode homoglyph / homograph structure in domain names, using Unicode UTS #39 17.0.0.
paypal.com
раypal.com
Those strings are not the same domain. The second mixes Cyrillic letters that UTS #39 maps as confusable with Latin.
It is for developers who accept domains through email fields, URL fields, registration forms, and similar inputs.
Implementations
One GitHub repository, ipfy/Homoglyph, ships native packages:
| Language | Package | Install |
|---|---|---|
| JavaScript | npm @ipfy/homoglyph |
npm install @ipfy/homoglyph |
| Python | PyPI ipfy-homoglyph |
pip install ipfy-homoglyph |
| PHP | Packagist ipfy/homoglyph |
composer require ipfy/homoglyph |
All three implement the same Homoglyph specification (see docs/DESIGN.md and docs/ARCHITECTURE.md), the same Unicode 17.0.0 confusable data, and the shared corpus in conformance/cases/.
JavaScript additionally provides browser warn() / reject() helpers. Python and PHP provide the core detect() API only.
What it is — and is not
The security boundary is the domain. Email local-parts and URL paths, queries, and fragments are not treated as hostname evidence.
| Observation | Meaning |
|---|---|
| Unicode in a domain | Not malicious. IDNs are legitimate. |
| Non-Latin | Not malicious. |
Punycode / xn-- |
Not malicious. Valid A-labels are decoded, then analyzed. |
| Mixed script | Not automatically malicious. |
detected: true |
A UTS #39-relevant issue worth reporting |
rejectable: true |
High-confidence finding suitable for hard enforcement |
Homoglyph does not decide whether a domain is a phishing site. It is not a blacklist, reputation service, DNS client, or TLD policy engine.
JavaScript (npm)
npm install @ipfy/homoglyph
import Homoglyph from '@ipfy/homoglyph'; const result = Homoglyph.detect('раypal.com'); if (result.detected) { console.log(result.reason, result.domain, result.rejectable); }
CDN (IIFE, globalThis.Homoglyph):
<script src="https://cdn.jsdelivr.net/npm/@ipfy/homoglyph/dist/homoglyph.min.js"></script>
Browser-only helpers: Homoglyph.warn(input) and Homoglyph.reject(input). See below. There is no init().
Python (PyPI)
pip install ipfy-homoglyph
import homoglyph result = homoglyph.detect("раypal.com") if result.detected: print(result.reason, result.domain, result.rejectable)
PHP (Packagist)
Requires PHP 8.1+ and ext-intl (Unicode NFC/NFD via Normalizer).
composer require ipfy/homoglyph
use IPfy\Homoglyph\Homoglyph; $result = Homoglyph::detect('раypal.com'); if ($result->detected) { echo $result->reason, ' ', $result->domain; }
Email and URL behavior
user@раypal.com → analyze раypal.com
mаnοj@paypal.com → domain paypal.com; not rejectable
https://раypal.com/login → analyze раypal.com
https://example.com/раypal → path ignored
https://example.com/?q=раypal → query ignored
https://example.com/#раypal → fragment ignored
пример.com → labels analyzed independently (not mixed-script)
Punycode / IDNA
Valid xn-- A-labels are decoded and then analyzed. Punycode itself is not evidence of malice. Successful Punycode decoding is not full IDNA validation.
Unicode
| Item | Value |
|---|---|
| Unicode / UTS #39 | 17.0.0 |
| Source of truth | unicode/ in this repository |
| Runtime download | none |
Refresh generated maps with npm run generate (writes JavaScript, Python, and PHP runtime data from the same files).
JavaScript warn() / reject()
These exist only in the JavaScript browser build. They attach form metadata or constraint validation from detect(). Visitor-facing reject() messages are ordinary validation text, not “phishing” warnings.
Development
npm install npm run generate npm test npm run build cd python && python -m build && pytest composer install && vendor/bin/phpunit -c php/phpunit.xml.dist
License
MIT. Unicode data: Unicode License V3 (see NOTICE).