In 2017 a researcher registered a domain that displayed as apple.com and was not apple.com. Every character in it was Cyrillic. Browsers patched it, the story was written up everywhere, and the matter was filed as closed.

It is not closed. The same trick still renders as Unicode today — it depends entirely on which top-level domain you put it under, and both major browsers ship test files that say so explicitly.

Why the original worked

The obvious version of this attack mixes alphabets: one Cyrillic а inside an otherwise Latin word. That version is easy to stop, and it was stopped years ago — you look for a string drawing on two scripts and refuse to render it.

The 2017 label defeated that by not mixing anything. а, р, р, ӏ, е were all Cyrillic. The string is perfectly consistent; it is simply a Cyrillic word that happens to be shaped like an English one.

Unicode's own security standard has a name for this — a whole-script confusable — and a scale of restriction levels for identifiers. Run the label against those levels and it passes all of them except "ASCII only" — Single Script, Highly Restrictive and Moderately Restrictive are all fine. There is nothing inconsistent for a script-mixing check to detect.

The domain is still registered, incidentally, with an expiry in January 2027.

The registry rule stops the wrong attack

You might expect the registry to have refused it. Its rules are public: the .com Cyrillic table lists 231 permitted code points — Cyrillic letters, digits and the hyphen, and no Latin letters at all.

That table is built to prevent script mixing. A Latin letter cannot appear beside a Cyrillic one in the same label. It does nothing about a label that is entirely Cyrillic and entirely misleading, because such a label breaks no rule. Every character of the 2017 demonstration appears in that permitted list, the rare ӏ included.

The governing guidelines have the same shape. Registries must take all code points in a label from one script, but are only encouraged to constrain whole-script confusables. The mandatory rule covers the script-mixing attack that browsers already stop; the attack that actually worked is covered only by a recommendation.

What the browsers really do

Because the registries do not stop this, browsers have to. Their mechanism is not a general rule but a specific list.

Both major browsers permit whole-script Cyrillic labels to render under a short set of top-level domains — the Bulgarian, Belarusian, Kazakh, Russian, Soviet-era, Ukrainian and Uzbek ones — and punycode them everywhere else. The two lists are identical, because one browser's spoof-check corpus was imported from the other's.

The clearest statement of this is in the browsers' own test data. The same all-Cyrillic string that reads as "scope" is marked unsafe on .com and safe on .ru. One string, two verdicts, decided by the suffix. That is deliberate: a Russian-language site under a Russian top-level domain has an honest reason to be written in Cyrillic, and refusing to render it would break the web for everyone who reads it.

So "browsers fixed it" is too coarse to be useful. They built a conditional defence: a hand-maintained enumeration, extended reactively as new cases appear.

One browser takes a stricter line: its allowed-script list omits Cyrillic and Greek entirely, so it punycodes labels the others display.

The comparison itself moved in 2025

Underneath all of this sits a data file. Unicode publishes a list of confusable characters, and the algorithm reduces a string to a "skeleton" by replacing each character with a prototype: the Cyrillic а becomes Latin a, and two strings are confusable when their skeletons match.

That file is a moving target.

In Unicode 16.0.0 there were 6,355 mappings. In 17.0.0 there are 6,565 — 210 added, and 39 existing mappings retargeted to a different prototype. One of the 39 is the Cyrillic palochka, ӏ, whose prototype changed from Latin i to Latin l.

That character is in the 2017 demonstration label.

Under Unicode 16.0.0, the skeleton of that famous label was appie, which does not match apple. Under 17.0.0 it is apple, which does.

The most-cited lookalike domain in the history of the problem did not formally register as a lookalike, by this measure, until Unicode 17.0.0 in September 2025. The attack worked from the day it was published; the standard's own comparison caught up eight years later.

The practical instruction follows directly, and the standard states it: never store a skeleton. Recompute it whenever you update Unicode, or you are matching against last year's idea of what looks alike.

Normalising the string is not enough, in both directions

The usual first instinct is Unicode normalisation — NFKC — which folds fullwidth and mathematical letters back to plain ASCII.

It does not touch the cases that matter. The Cyrillic а, the Greek ο, the Cyrillic е, р and с all survive NFKC completely unchanged, because they are not compatibility variants of anything. They are separate letters that happen to be drawn the same way. Across the full confusables list, more than half of the mappings need the skeleton algorithm and cannot be reached by normalisation.

A reverse gap exists as well. Fullwidth letters are in the confusables file, but fullwidth digits are not. So a fullwidth 0 folds under NFKC and is left untouched by the skeleton. Neither transform contains the other, and a defence using only one has a hole shaped like the other.

It is not only about domains, and mostly not about Unicode

The four largest package registries — npm, PyPI, RubyGems and crates.io — reject non-ASCII package names outright, so the Unicode homoglyph attack cannot be expressed there at all. The real typosquats in those ecosystems are plain ASCII: a capital I standing in for a lowercase l.

This is why one detail of the confusables file matters more than it seems: eight of its source characters are plain ASCII. 0 maps to O, 1 and I and the pipe all map to l, and m maps to the two-character sequence rn. The same algorithm built for cross-script attacks catches the oldest typosquat in the book.

One large registry does permit Unicode identifiers, and that is where the genuine homoglyph incidents have occurred — a package published in October 2025 using a Cyrillic е inside an otherwise Latin name, removed four days later.

⚠️ One distinction to keep straight, because it is routinely collapsed. "Trojan Source" is a different attack with its own advisory. It uses bidirectional control characters to make source code render in a different order from the order the compiler reads. Nothing there looks like anything else — the deception is positional, not visual. The two are related only in that the modern skeleton definition runs the bidirectional algorithm first.

If you are defending something

Compare skeletons rather than raw strings, and case-fold on both sides of the skeleton rather than only before it — because 0 maps to a capital O, folding only at the start misses g00gle against google.

Apply the identifier profile as well. It is not redundant with the skeleton: it removes the characters normalisation would already have handled and leaves precisely the cross-script letters that only skeleton comparison catches.

And accept that restriction levels cannot help with whole-script confusables. The standard's own example is a Cyrillic string that reads as "Circle" — single-script, well-formed, passes the levels, and skeleton-identical to the Latin word. If your check only asks "is this string internally consistent?", a well-built impersonation will always pass.

Where this comes from, and what will date it

The mapping counts, the 39 retargetings, the palochka change and the skeletons of the demonstration label under both Unicode versions were computed by parsing the two published data files directly, not read from anyone's description of them. The eight ASCII source rows were extracted the same way. The registry code-point table and the guidelines wording are the published primary documents.

Two limits. We did not reach a first-party incident report for any recent lookalike-domain campaign, so this guide names none — vendor write-ups exist and are not primary. And whether the permitted country-code registries allow Latin-lookalike labels in their own tables was not checked; only the .com table was.

The specific mappings will be dated by the next Unicode release, and the allow-list of top-level domains will change slowly and by hand. The structural point remains: a defence assembled from an enumeration is only ever current, never complete.