From 90cfc7ab1f968bb5c0f7260ba3722aefb2cd7daf Mon Sep 17 00:00:00 2001
From: jyn88
Date: Tue, 9 Jun 2026 15:52:01 +0800
Subject: [PATCH] merge patch

---
 idna/core.py       | 23 ++++++++++++++++++++++-
 tests/test_idna.py | 37 +++++++++++++++++++++++++++++++++++++
 2 files changed, 59 insertions(+), 1 deletion(-)

diff --git a/idna/core.py b/idna/core.py
index 0dae61a..28c5db3 100644
--- a/idna/core.py
+++ b/idna/core.py
@@ -229,7 +229,18 @@ def check_label(label: Union[str, bytes, bytearray]) -> None:
     if isinstance(label, (bytes, bytearray)):
         label = label.decode('utf-8')
     if len(label) == 0:
-        raise IDNAError('Empty Label')
+        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)
@@ -340,6 +351,12 @@ def encode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool =
             raise IDNAError('should pass a unicode string to the function rather than a byte string.')
     if uts46:
         s = uts46_remap(s, std3_rules, transitional)
+
+    # Reject inputs that exceed the maximum DNS domain length up-front
+    # to avoid expensive computation on long inputs.
+    if not valid_string_length(s, trailing_dot=True):
+        raise IDNAError("Domain too long")
+
     trailing_dot = False
     result = []
     if strict:
@@ -373,6 +390,10 @@ def decode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool =
         raise IDNAError('Invalid ASCII in A-label')
     if uts46:
         s = uts46_remap(s, std3_rules, False)
+    # Reject inputs that exceed the maximum DNS domain length up-front
+    # to avoid expensive computation on long inputs.
+    if not valid_string_length(s, trailing_dot=True):
+        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 81afb32..2dc0892 100755
--- a/tests/test_idna.py
+++ b/tests/test_idna.py
@@ -78,6 +78,43 @@ 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_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 = '\u0061'
-- 
2.52.0.windows.1