Skip to content

Reject IDNA-mapped lookalikes in domains and simple hostnames - #527

Merged
nandgator merged 1 commit into
masterfrom
fix-idna-lookalike-domains
Oct 22, 2026
Merged

nandgator merged 1 commit into
masterfrom
fix-idna-lookalike-domains

Conversation

@nandgator

Copy link
Copy Markdown
Collaborator

Python's idna codec implements IDNA 2003 (RFC 3490). Its nameprep step (RFC 3491) silently drops invisible characters (RFC 3454 table B.1), folds compatibility characters to ASCII, and RFC 3490 section 3.1 makes U+3002, U+FF0E and U+FF61 label separators. domain() validated the mapped result, so names that only look like another name passed domain(), hostname(), url() and email():

>>> validators.domain("example.com")        # fullwidth
True
>>> validators.domain("exam­ple.com")    # soft hyphen
True
>>> validators.domain("example。com")      # ideographic full stop
True
>>> validators.hostname("ſerver")         # long s, simple hostname
True

With consider_tld=True, the raw-TLD lookup happened to reject some of these, but only when the lookalike was in the TLD. #523 moves the lookup to the encoded TLD, which would remove that accidental protection, so this should land first.

Change

  • domain() rejects non-ASCII values that are not NFKC-stable, or that contain a character nameprep maps to nothing or treats as a dot. IDNA 2008 (RFC 5894 section 7.3) drops these mappings as well.
  • The simple hostname regex gets re.ASCII. Without it, [a-z] with re.IGNORECASE also matches U+0130, U+0131, U+017F and U+212A (Python re docs). This is the same issue Reject non-ASCII case equivalents in IBANs #512 fixes for IBANs.

Internationalized names that need no such mapping keep validating, e.g. пример.рф, ПРИМЕР.РФ, 例子.中国, kräuter.com and straße.de. All 151 IDN TLDs in the current IANA list pass, both alone and with #523 applied under consider_tld=True.

Tests

New cases in test_domain.py, test_hostname.py, test_url.py and test_email.py fail on master and pass here. The full suite passes (1518 tests), as do ruff check, ruff format and pyright.

Python's idna codec implements IDNA 2003, whose nameprep step drops
invisible characters (soft hyphen, zero width space, joiners, variation
selectors), folds compatibility characters such as fullwidth letters,
the Kelvin sign or the long s to ASCII, and splits labels on U+3002 and
similar dots. domain() validated the mapped result, so "example.com"
or "exam­ple.com" passed as example.com through domain(),
hostname(), url() and email().

Reject non-ASCII values that are not NFKC-stable or that contain
characters nameprep maps to nothing or treats as a dot. Internationalized
names that need no such mapping, such as "пример.рф" or "straße.de",
are still accepted.

The simple hostname regex used [a-z] with re.IGNORECASE, which also
matches U+0130, U+0131, U+017F and U+212A; add re.ASCII.
@nandgator
nandgator force-pushed the fix-idna-lookalike-domains branch from 13a5cdb to 3c2413d Compare October 22, 2026 02:00
@nandgator
nandgator merged commit 8a5b260 into master Oct 22, 2026
6 checks passed
@nandgator
nandgator deleted the fix-idna-lookalike-domains branch October 22, 2026 02:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant