From 320f91cf06a927f48d164ed505e798955ddfd45a Mon Sep 17 00:00:00 2001 From: Kim Davies Date: Sun, 10 May 2026 08:47:22 -0700 Subject: [PATCH 1/3] Merge commit from fork --- idna/core.py | 14 ++++++++++++++ tests/test_idna.py | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/idna/core.py b/idna/core.py index 1d36686..fe55b5b 100644 --- a/idna/core.py +++ b/idna/core.py @@ -342,6 +342,15 @@ def encode(s, strict=False, uts46=False, std3_rules=False, transitional=False): s = s.decode("ascii") if uts46: s = uts46_remap(s, std3_rules, transitional) + + # Reject inputs that exceed the maximum DNS domain length up-front. + # Each codepoint in a U-label contributes at least one octet to its + # A-label form, so any input longer than the domain limit cannot + # produce a valid A-domain. Short-circuiting here prevents per-label + # validation from being driven into quadratic time + if len(s) > 254: + raise IDNAError("Domain too long") + trailing_dot = False result = [] if strict: @@ -371,6 +380,11 @@ def decode(s, strict=False, uts46=False, std3_rules=False): s = s.decode("ascii") if uts46: s = uts46_remap(s, std3_rules, False) + # See encode() for rationale; the same bound applies because every + # legal A-domain is at most 254 octets and every codepoint of a + # legal U-domain contributes at least one octet to its A-form. + if len(s) > 254: + raise IDNAError("Domain too long") trailing_dot = False result = [] if not strict: diff --git a/tests/test_idna.py b/tests/test_idna.py index 061f129..ff60444 100755 --- a/tests/test_idna.py +++ b/tests/test_idna.py @@ -84,6 +84,19 @@ class IDNATests(unittest.TestCase): self.assertFalse(idna.valid_label_length('a' * 64)) self.assertRaises(idna.IDNAError, idna.encode, 'a' * 64) + def test_oversized_input_rejected_promptly(self): + # GHSA-65pc-fj4g-8rjx: encode/decode must reject inputs that + # exceed the maximum DNS domain length before per-codepoint + # validation runs, so labels dominated by CONTEXTO codepoints + # cannot drive validation into quadratic time. + import time + + for payload in ("٠" * 8000, "・" * 8000 + "漢"): + start = time.perf_counter() + self.assertRaises(idna.IDNAError, idna.encode, payload) + self.assertRaises(idna.IDNAError, idna.decode, payload) + self.assertLess(time.perf_counter() - start, 1.0) + def test_check_bidi(self): l = u'\u0061' From cfdb7a332e69b7664762bc2fdd63e51f8fb75050 Mon Sep 17 00:00:00 2001 From: metsw24-max Date: Mon, 11 May 2026 20:59:30 +0530 Subject: [PATCH 2/3] Enforce early length limits in check_label --- idna/core.py | 11 +++++++++++ tests/test_idna.py | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/idna/core.py b/idna/core.py index fe55b5b..48e61d3 100644 --- a/idna/core.py +++ b/idna/core.py @@ -238,6 +238,17 @@ def check_label(label): label = label.decode('utf-8') if len(label) == 0: raise IDNAError('Empty Label') + # Reject oversized labels before per-codepoint validation runs. + # CONTEXTJ/CONTEXTO checks scan the whole label per codepoint, so an + # uncapped label drives validation into quadratic time + # (GHSA-65pc-fj4g-8rjx / CVE-2024-3651). encode()/decode() cap the + # whole-domain length; this cap protects direct callers of + # alabel/ulabel/check_label and the idna2008 incremental codec. + # Use the whole-domain bound rather than the per-label DNS bound so + # that UTS #46 lenient decoding of labels longer than 63 chars is + # preserved. + if not valid_string_length(label, trailing_dot=True): + raise IDNAError('Label too long') check_nfc(label) check_hyphen_ok(label) diff --git a/tests/test_idna.py b/tests/test_idna.py index ff60444..4195075 100755 --- a/tests/test_idna.py +++ b/tests/test_idna.py @@ -97,6 +97,30 @@ class IDNATests(unittest.TestCase): self.assertRaises(idna.IDNAError, idna.decode, payload) self.assertLess(time.perf_counter() - start, 1.0) + def test_oversized_label_rejected_promptly(self): + # The whole-domain cap in encode()/decode() does not cover direct + # callers of alabel/ulabel/check_label, nor the idna2008 + # incremental codec which calls alabel/ulabel per label. Without a + # per-label cap, a single oversized CONTEXTO-heavy label still + # drives validation into quadratic time. + import codecs + import time + + import idna.codec # noqa: F401 (register the idna2008 codec) + + payload = "・" * 8000 + "漢" + start = time.perf_counter() + self.assertRaises(idna.IDNAError, idna.check_label, payload) + self.assertRaises(idna.IDNAError, idna.alabel, payload) + self.assertRaises(idna.IDNAError, idna.ulabel, payload) + self.assertRaises( + idna.IDNAError, + codecs.getincrementalencoder("idna2008")().encode, + payload, + True, + ) + self.assertLess(time.perf_counter() - start, 1.0) + def test_check_bidi(self): l = u'\u0061' From c3c00c307f58a72ccfad94b6a51ef40094368c15 Mon Sep 17 00:00:00 2001 From: RHEL Packaging Agent Date: Mon, 27 Jul 2026 07:12:18 +0000 Subject: [PATCH 3/3] Adapt test to v2.5 codec API: use idna.codec.IncrementalEncoder directly In v2.5, the codec is named 'idna' (not 'idna2008') and uses the old-style getregentry() mechanism rather than codecs.register(). Use the codec class directly instead of going through codecs.getincrementalencoder('idna2008'). --- tests/test_idna.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_idna.py b/tests/test_idna.py index 4195075..247f7f9 100755 --- a/tests/test_idna.py +++ b/tests/test_idna.py @@ -103,10 +103,9 @@ class IDNATests(unittest.TestCase): # incremental codec which calls alabel/ulabel per label. Without a # per-label cap, a single oversized CONTEXTO-heavy label still # drives validation into quadratic time. - import codecs import time - import idna.codec # noqa: F401 (register the idna2008 codec) + import idna.codec payload = "・" * 8000 + "漢" start = time.perf_counter() @@ -115,7 +114,7 @@ class IDNATests(unittest.TestCase): self.assertRaises(idna.IDNAError, idna.ulabel, payload) self.assertRaises( idna.IDNAError, - codecs.getincrementalencoder("idna2008")().encode, + idna.codec.IncrementalEncoder().encode, payload, True, )