python-idna/CVE-2026-45409.patch
RHEL Packaging Agent c86a94e122 Fix CVE-2026-45409: quadratic time complexity in IDNA validation
Add patch CVE-2026-45409.patch to enforce early length limits in
encode(), decode(), and check_label() so that CONTEXTO-heavy
inputs cannot drive per-codepoint validation into quadratic
time. The patch also adapts the upstream test to work with the
v2.5 codec API by using idna.codec.IncrementalEncoder directly
instead of codecs.getincrementalencoder("idna2008").

CVE: CVE-2026-45409
Upstream patches:
 - c0dda4501d.patch
 - e1cb465b63.patch
Resolves: RHEL-215650

This commit was backported by Ymir, a Red Hat Enterprise Linux software maintenance AI agent.

Assisted-by: Ymir
Co-authored-by: Tomáš Hrnčiar <thrnciar@redhat.com>
2026-08-06 11:12:52 +02:00

174 lines
6.7 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

From 320f91cf06a927f48d164ed505e798955ddfd45a Mon Sep 17 00:00:00 2001
From: Kim Davies <kim@cynosure.com.au>
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 <metsw24@gmail.com>
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 <redhat-ymir-agent@redhat.com>
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,
)