Skip to content

Commit 8d1fbd2

Browse files
authored
Apply the IDNA Bidi Rule to every label of a Bidi domain (#188)
The idna package checks the RFC 5893 Bidi Rule only on labels that contain right-to-left characters. RFC 5893 and UTS #46 CheckBidi require every label to satisfy it once any label does, so domains like 1.אב.com and אב.1.com were both accepted although they render identically.
1 parent 4ac100e commit 8d1fbd2

3 files changed

Lines changed: 21 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ In Development
55
* The `original` field of the returned validated email object incorrectly un-escaped the local part of the email address. Now it returns the original address, without any display name, as passed to validate_email.
66
* add support python3.15
77
* The IPv6 tag of a domain literal is now case-insensitive, as in the RFCs, so `me@[ipv6:::1]` is accepted (and normalized to `me@[IPv6:::1]`) when `allow_domain_literal=True`.
8+
* Internationalized domains with a right-to-left label are now rejected if any other label breaks the IDNA Bidi Rule (RFC 5893), such as a label starting with a digit. Such domains, like `1.xn--4dbc.com` and `xn--4dbc.1.com` (`1.אב.com` and `אב.1.com` in Unicode), could be displayed identically.
89

910
2.3.0 (August 26, 2025)
1011
-----------------------

‎email_validator/syntax.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,20 @@ def validate_email_domain_name(domain: str, test_environment: bool = False, glob
675675
raise EmailSyntaxError("The part after the @-sign contains invalid characters: " + ", ".join(sorted(bad_chars)) + ".")
676676
check_unsafe_chars(domain_i18n)
677677

678+
# If any label has right-to-left characters, the Bidi Rule applies to
679+
# every label in the domain, including left-to-right ones (RFC 5893
680+
# Section 1.4 and 2, UTS #46 CheckBidi). The idna package only checks
681+
# each label on its own, so a label like "1" next to a right-to-left
682+
# label was accepted, and such domains can be displayed identically to
683+
# a different domain (e.g. 1.xn--4dbc.com and xn--4dbc.1.com, which
684+
# are 1.אב.com and אב.1.com in Unicode, with Hebrew alef bet).
685+
if any(unicodedata.bidirectional(c) in ("R", "AL", "AN") for c in domain_i18n):
686+
for label in domain_i18n.split("."):
687+
try:
688+
idna.check_bidi(label, check_ltr=True)
689+
except idna.IDNAError as e:
690+
raise EmailSyntaxError(f"The part after the @-sign is not valid IDNA ({e}).") from e
691+
678692
# Check that it can be encoded back to IDNA ASCII. We have no test
679693
# case for this.
680694
try:

‎tests/test_syntax.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ def test_domain_literal() -> None:
478478
('meme@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.com', 'The email address is too long (4 characters too many).'),
479479
('my.long.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.11111111112222222222333333333344444.info', 'The email address is too long (2 characters too many).'),
480480
('my.long.address@λ111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info', 'The email address is too long (1-2 characters too many).'),
481-
('my.long.address@\uFB2C111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info', 'The email address is too long (1-3 characters too many).'),
481+
('my.long.address@\uFB2C111111111222222222233333333334444444444555555555.a666666666777777777788888888889999999999000000000.a111111111222222222233333333334444444444555555555.a666666666777777777788888888889999999999000000000.a111111111222222222233333333334444.info', 'The email address is too long (1-3 characters too many).'),
482482
('my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.111111111122222222223333333333444.info', 'The email address is too long (1 character too many).'),
483483
('my.λong.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info', 'The email address is too long (1-2 characters too many).'),
484484
('my.\u0073\u0323\u0307.address@1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444444444555555555.6666666666777777777788888888889999999999000000000.1111111111222222222233333333334444.info', 'The email address is too long (1-2 characters too many).'),
@@ -489,6 +489,11 @@ def test_domain_literal() -> None:
489489
('me@bad-tld-1', 'The part after the @-sign is not valid. It should have a period.'),
490490
('me@bad.tld-2', 'The part after the @-sign is not valid. It is not within a valid top-level domain.'),
491491
('me@xn--0.tld', 'The part after the @-sign is not valid IDNA (Invalid A-label).'),
492+
# 1.xn--4dbc.com is 1.אב.com and xn--4dbc.1.com is אב.1.com
493+
# (alef bet in Hebrew). Both display as the same string, so both are rejected.
494+
('me@1.xn--4dbc.com', 'The part after the @-sign is not valid IDNA (First codepoint in label \'1\' must be directionality L, R or AL).'),
495+
('me@xn--4dbc.1.com', 'The part after the @-sign is not valid IDNA (First codepoint in label \'1\' must be directionality L, R or AL).'),
496+
('me@1.xn--4dbrk0ce', 'The part after the @-sign is not valid IDNA (First codepoint in label \'1\' must be directionality L, R or AL).'),
492497
('me@yy--0.tld', 'An email address cannot have two letters followed by two dashes immediately after the @-sign or after a period, except Punycode.'),
493498
('me@yy--0.tld', 'An email address cannot have two letters followed by two dashes immediately after the @-sign or after a period, except Punycode.'),
494499
('me@[127.0.0.1]', 'A bracketed IP address after the @-sign is not allowed here.'),

0 commit comments

Comments
 (0)