What is Antispoof?
AntiSpoof https://www.mediawiki.org/wiki/Extension:AntiSpoof
AntiSpoof is an extension for preventing confusable usernames from being created. It blocks the creation of accounts with mixed-script, confusing and similar usernames.
For example, if user John Doe is already registered, the extension will block attempts to register:
- joHn dOE (capital letters in different places)
- Jοhn Doe ("ο" in Jοhn is Greek small letter omicron)
- John Dоe ("о" in Dоe is Cyrillic small letter O)
- Јohn Doe ("Ј" in Јohn is Cyrillic capital letter Je)
- John Đoe ("Đ" in Đoe is Latin capital letter D with stroke)
- Jóhn Doe ("ó" in Jóhn is Latin small letter O with acute)
- John Doe (there is a Zero-width space before Doe)
- Jøhn Doe ("ø" in Jøhn is Scandinavian oe)
- J0hn Doe ("0" is the number zero)
etc.
AntiSpoof relies on Equivset.
What is Equivset?
Equivset https://www.mediawiki.org/wiki/Equivset
Equivset is a library for detecting visually similar UTF-8 characters.
Equivset is designed to prevent abuse through imitation of words and focuses primarily on letters and punctuation (not emojis or other symbols). It contains mapping of visually identical characters from Unicode Confusables such as Latin "A" and Greek "Α" (alpha), as well as additional mapping for visually similar characters such as "S" and "$" (dollar sign).
Equivset is implicitly used by AntiSpoof, and it is available for use in AbuseFilter, where character normalization can be invoked using the ccnorm function. The result of normalization of any string can be tested anytime using Special:AbuseFilter/tools.
How is AntiSpoof integrated in TitleBlacklist?
Using TitleBlacklist, admins (stewards) can specify which titles cannot be created through regex rules. For example:
.*wikipedia.* <autoconfirmed>
will block creating titles (and user accounts) which contain "wikipedia" (regardless of case) for non-autoconfirmed users.
Now this can be easily bypassed by changing a character to its visually similar counterpart, e.g., replacing the Latin 'i' with Cyrillic 'і'. Or 'k' with Greek 'κ'. In fact, every character from the English alphabet can be "spoofed" by many other characters. Maintaining regexes covering all possibilities, as they come, would require much effort, and such regexes would become unreadable and inefficient.
Imagine we'd like to bypass the rule by creating the page called "ωіκiρEd1а". This is where Equivset and AntiSpoof come in handy. Title blacklist supports per-entry 'antispoof' flag, which normalizes the title before attempting to match the regex. Since both ccnorm('wikipedia') and ccnorm('ωіκiρEd1а') evaluate to WIKIPEDIA, the entry can be modified to something like:
.*WIKIPEDIA.* <antispoof|casesensitive|autoconfirmed>
So what is wrong?
The above is a description of the ideal behavior, or perhaps how others think it works (like I did), but in reality, it's somewhat flawed.
First, there is no documentation of the normalization algorithm. Users are given this clue:
If the AntiSpoof extension is installed, the antispoof attribute can be used to normalize titles before checking them against the rules.
and also that Equivset (better known from AbuseFilter) is somehow used inside.
AntiSpoof also provides the action=antispoof API module, which can be used to test if the provided username conflicts with an existing one (and could be used for spoofing it).
Query for "wikipedia" returns
{ "antispoof": { "username": "wikipedia", "normalised": "v2:WIKIPEDIA", "result": "conflict", "users": [ "WiKiPeDia" ] } }
However, query for "ωіκiρEd1а" returns
{ "antispoof": { "username": "ωіκiρEd1а", "result": "error", "error": "Usernames cannot contain characters from different writing systems" } }
This corresponds to the declared purpose of AntiSpoof:
It blocks the creation of accounts with mixed-script, confusing and similar usernames.
But the above still does not answer how this works with the title blacklist. Therefore, I had a look at the code and was very surprised.
Inside TitleBlacklist, we find (simplified):
if ( isset( $this->mParams['antispoof'] ) ) { $status = AntiSpoof::checkUnicodeStringStatus( $title ); if ( $status->isOK() ) { // Remove version from return value [ , $title ] = explode( ':', $status->getValue(), 2 ); } else { wfDebugLog( 'TitleBlacklist', 'AntiSpoof could not normalize "' . $title . '" ' . $status->getMessage( false, false, 'en' )->text() . '.' ); } } $match = preg_match( "/^(?:{$this->mRegex})$/us" . ( isset( $this->mParams['casesensitive'] ) ? '' : 'i' ), $title );
Most of the business logic apparently lies in AntiSpoof's ::checkUnicodeStringStatus function.
In particular, notice the following snippet:
if ( count( $testScripts ) > 1 && !self::isAllowedScriptCombination( $testScripts ) ) { return Status::newFatal( 'antispoof-mixedscripts' ); }
antispoof-mixedscripts resolves to "Usernames cannot contain characters from different writing systems", hence these are the lines that caused the above action=antispoof&username=ωіκiρEd1а API call to return an error.
However, this also means that when title blacklist calls the function with the same input, it receives the same error and, most importantly, no normalization is done. The error is silently ignored, and the regex tries to match the unnormalized, possibly "spoofed" title.
In other words, Antispoof is smart enough to identify a string consisting of letters from different alphabets as malicious (spoofed) and reject it.
But TitleBlacklist will treat the response as an error and try to match regexes to the unnormalized string.
These regexes, if simplified to match against normalized strings like "WIKIPEDIA", are then ineffective.
What should be done with it?
- Factor the useful logic out of AntiSpoof::checkUnicodeStringStatus for use in TitleBlacklist, while preserving the current AntiSpoof's functionality.
- Reimplement string normalization using routines from AbuseFilter (norm, ccnorm, rmspecials) and deprecate antispoof.
- Merge TitleBlacklist into AbuseFilter, retire it (T279275).
See also: