mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-10-20 07:57:44 +00:00
python3-twisted: Fix CVE-2026-42304
This patch applies the upstream 26.4.0rc2 backport for CVE-2026-42304. The upstream fix merge is referenced in [1], and the public CVE advisory is referenced in [2]. The individual backported commit links are recorded in the patch headers. [1] https://github.com/twisted/twisted/commit/2d196123264efb0027eecfe1b430be4a9babdbd8 [2] https://github.com/advisories/GHSA-grgv-6hw6-v9g4 Signed-off-by: Hetvi Thakar <hthakar@cisco.com> Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
1 parent
8519105eec
commit
2fb2b9fb1d
7 files changed
+929
No files matched your search
@@ -0,0 +1,299 @@
|
|||||||
|
From a023e4192dbbdc7d8d6b391ef9b226c2cfffe97e Mon Sep 17 00:00:00 2001
|
||||||
|
From: tomasilluminati <tomas.illuminati@owasp.org>
|
||||||
|
Date: Sun, 19 Apr 2026 05:57:33 -0300
|
||||||
|
Subject: [PATCH] (fix): denial of service in twisted.names mitigation
|
||||||
|
|
||||||
|
CVE: CVE-2026-42304
|
||||||
|
Upstream-Status: Backport [https://github.com/twisted/twisted/commit/be71ecaa113f642f03083bd5ed33af47c59308c8]
|
||||||
|
|
||||||
|
(cherry picked from commit be71ecaa113f642f03083bd5ed33af47c59308c8)
|
||||||
|
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||||
|
---
|
||||||
|
src/twisted/names/dns.py | 116 +++++++++++++++++++++++++----
|
||||||
|
src/twisted/names/test/test_dns.py | 91 ++++++++++++++++++++++
|
||||||
|
2 files changed, 193 insertions(+), 14 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/twisted/names/dns.py b/src/twisted/names/dns.py
|
||||||
|
index c7644ef50..7142d9e75 100644
|
||||||
|
--- a/src/twisted/names/dns.py
|
||||||
|
+++ b/src/twisted/names/dns.py
|
||||||
|
@@ -11,6 +11,7 @@ Future Plans:
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# System imports
|
||||||
|
+import contextvars
|
||||||
|
import inspect
|
||||||
|
import random
|
||||||
|
import socket
|
||||||
|
@@ -126,6 +127,7 @@ __all__ = [
|
||||||
|
"OP_UPDATE",
|
||||||
|
"PORT",
|
||||||
|
"AuthoritativeDomainError",
|
||||||
|
+ "DNSDecodeError",
|
||||||
|
"DNSQueryTimeoutError",
|
||||||
|
"DomainError",
|
||||||
|
]
|
||||||
|
@@ -444,6 +446,64 @@ def readPrecisely(file, l):
|
||||||
|
return buff
|
||||||
|
|
||||||
|
|
||||||
|
+# Cap the total number of compression-pointer dereferences performed while
|
||||||
|
+# decoding a single DNS message. A hostile peer can otherwise craft a packet
|
||||||
|
+# in which every record name chases a long compression chain, forcing O(N*M)
|
||||||
|
+# work and stalling the reactor.
|
||||||
|
+MAX_COMPRESSION_POINTERS_PER_MESSAGE = 1000
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+class DNSDecodeError(ValueError):
|
||||||
|
+ """
|
||||||
|
+ Raised when a DNS message cannot be decoded because it violates a
|
||||||
|
+ protocol-level safety limit
|
||||||
|
+ """
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+class _DecodeContext:
|
||||||
|
+ """
|
||||||
|
+ Mutable state shared between the L{IEncodable} decoders invoked while
|
||||||
|
+ reading a single DNS message.
|
||||||
|
+
|
||||||
|
+ The primary purpose is to bound the total number of compression-pointer
|
||||||
|
+ jumps taken across every name in the message, defending against packets
|
||||||
|
+ that fan out thousands of records pointing to deeply chained pointers.
|
||||||
|
+
|
||||||
|
+ @ivar jumps: The number of compression pointers followed so far.
|
||||||
|
+ @ivar maxJumps: The inclusive upper bound on C{jumps}. Exceeding it
|
||||||
|
+ causes L{registerJump} to raise L{DNSDecodeError}.
|
||||||
|
+ """
|
||||||
|
+
|
||||||
|
+ __slots__ = ("jumps", "maxJumps")
|
||||||
|
+
|
||||||
|
+ def __init__(self, maxJumps: int = MAX_COMPRESSION_POINTERS_PER_MESSAGE) -> None:
|
||||||
|
+ self.jumps = 0
|
||||||
|
+ self.maxJumps = maxJumps
|
||||||
|
+
|
||||||
|
+ def registerJump(self) -> None:
|
||||||
|
+ """
|
||||||
|
+ Record that a compression pointer has been followed
|
||||||
|
+
|
||||||
|
+ @raise DNSDecodeError: if the cumulative number of jumps exceeds
|
||||||
|
+ L{maxJumps}
|
||||||
|
+ """
|
||||||
|
+ self.jumps += 1
|
||||||
|
+ if self.jumps > self.maxJumps:
|
||||||
|
+ raise DNSDecodeError(
|
||||||
|
+ "Too many compression pointers while decoding DNS message "
|
||||||
|
+ f"(limit is {self.maxJumps})"
|
||||||
|
+ )
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+# Tracks state across nested calls without altering every record's signature.
|
||||||
|
+# L{Message.decode} manages the lifecycle per-message, while standalone decoders
|
||||||
|
+# default to a local context when C{_decodeContextVar} is C{None}
|
||||||
|
+
|
||||||
|
+_decodeContextVar: contextvars.ContextVar[_DecodeContext | None] = (
|
||||||
|
+ contextvars.ContextVar("_dnsDecodeContext", default=None)
|
||||||
|
+)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
class IEncodable(Interface):
|
||||||
|
"""
|
||||||
|
Interface for something which can be encoded to and decoded
|
||||||
|
@@ -591,7 +651,7 @@ class Name:
|
||||||
|
strio.write(label)
|
||||||
|
strio.write(b"\x00")
|
||||||
|
|
||||||
|
- def decode(self, strio, length=None):
|
||||||
|
+ def decode(self, strio, length=None, context=None):
|
||||||
|
"""
|
||||||
|
Decode a byte string into this Name.
|
||||||
|
|
||||||
|
@@ -599,12 +659,27 @@ class Name:
|
||||||
|
@param strio: Bytes will be read from this file until the full Name
|
||||||
|
is decoded.
|
||||||
|
|
||||||
|
+ @type context: L{_DecodeContext} or L{None}
|
||||||
|
+ @param context: Shared decoding state used to cap the total number
|
||||||
|
+ of compression-pointer jumps taken while decoding the enclosing
|
||||||
|
+ DNS message. When L{None}, the context installed by
|
||||||
|
+ L{Message.decode} is used if one is active; otherwise a fresh,
|
||||||
|
+ call-local context is created so that direct callers remain
|
||||||
|
+ protected and backwards compatible.
|
||||||
|
+
|
||||||
|
@raise EOFError: Raised when there are not enough bytes available
|
||||||
|
from C{strio}.
|
||||||
|
|
||||||
|
- @raise ValueError: Raised when the name cannot be decoded (for example,
|
||||||
|
- because it contains a loop).
|
||||||
|
+ @raise ValueError: Raised when the name cannot be decoded because it
|
||||||
|
+ contains a compression loop.
|
||||||
|
+
|
||||||
|
+ @raise DNSDecodeError: Raised when the cumulative number of
|
||||||
|
+ compression-pointer jumps exceeds the configured limit.
|
||||||
|
"""
|
||||||
|
+ if context is None:
|
||||||
|
+ context = _decodeContextVar.get()
|
||||||
|
+ if context is None:
|
||||||
|
+ context = _DecodeContext()
|
||||||
|
visited = set()
|
||||||
|
self.name = b""
|
||||||
|
off = 0
|
||||||
|
@@ -616,6 +691,7 @@ class Name:
|
||||||
|
return
|
||||||
|
if (l >> 6) == 3:
|
||||||
|
new_off = (l & 63) << 8 | ord(readPrecisely(strio, 1))
|
||||||
|
+ context.registerJump()
|
||||||
|
if new_off in visited:
|
||||||
|
raise ValueError("Compression loop in encoded name")
|
||||||
|
visited.add(new_off)
|
||||||
|
@@ -2704,19 +2780,31 @@ class Message(tputil.FancyEqMixin):
|
||||||
|
self.checkingDisabled = (byte4 >> 4) & 1
|
||||||
|
self.rCode = byte4 & 0xF
|
||||||
|
|
||||||
|
- self.queries = []
|
||||||
|
- for i in range(nqueries):
|
||||||
|
- q = Query()
|
||||||
|
- try:
|
||||||
|
- q.decode(strio)
|
||||||
|
- except EOFError:
|
||||||
|
- return
|
||||||
|
- self.queries.append(q)
|
||||||
|
+ # A single shared counter bounds the total compression-pointer work
|
||||||
|
+ # performed across every name in this message. It is installed on
|
||||||
|
+ # the context variable so nested record decoders pick it up without
|
||||||
|
+ # needing to thread it through each signature.
|
||||||
|
+ token = _decodeContextVar.set(_DecodeContext())
|
||||||
|
+ try:
|
||||||
|
+ self.queries = []
|
||||||
|
+ for i in range(nqueries):
|
||||||
|
+ q = Query()
|
||||||
|
+ try:
|
||||||
|
+ q.decode(strio)
|
||||||
|
+ except EOFError:
|
||||||
|
+ return
|
||||||
|
+ self.queries.append(q)
|
||||||
|
|
||||||
|
- items = ((self.answers, nans), (self.authority, nns), (self.additional, nadd))
|
||||||
|
+ items = (
|
||||||
|
+ (self.answers, nans),
|
||||||
|
+ (self.authority, nns),
|
||||||
|
+ (self.additional, nadd),
|
||||||
|
+ )
|
||||||
|
|
||||||
|
- for l, n in items:
|
||||||
|
- self.parseRecords(l, n, strio)
|
||||||
|
+ for l, n in items:
|
||||||
|
+ self.parseRecords(l, n, strio)
|
||||||
|
+ finally:
|
||||||
|
+ _decodeContextVar.reset(token)
|
||||||
|
|
||||||
|
def parseRecords(self, list, num, strio):
|
||||||
|
for i in range(num):
|
||||||
|
diff --git a/src/twisted/names/test/test_dns.py b/src/twisted/names/test/test_dns.py
|
||||||
|
index 3b8f6e130..9b27c4b3a 100644
|
||||||
|
--- a/src/twisted/names/test/test_dns.py
|
||||||
|
+++ b/src/twisted/names/test/test_dns.py
|
||||||
|
@@ -352,6 +352,60 @@ class NameTests(unittest.TestCase):
|
||||||
|
stream = BytesIO(b"\xc0\x00")
|
||||||
|
self.assertRaises(ValueError, name.decode, stream)
|
||||||
|
|
||||||
|
+ def test_rejectTooManyCompressionPointers(self):
|
||||||
|
+ """
|
||||||
|
+ L{Name.decode} raises L{dns.DNSDecodeError} when the number of
|
||||||
|
+ compression-pointer dereferences taken for a single message exceeds
|
||||||
|
+ the limit carried by the shared L{dns._DecodeContext}.
|
||||||
|
+ """
|
||||||
|
+ # Five distinct pointers chained end-to-end, terminated by a zero
|
||||||
|
+ # label byte. With a maxJumps of three the fourth dereference must
|
||||||
|
+ # trip the safety limit.
|
||||||
|
+ payload = b"\xc0\x02\xc0\x04\xc0\x06\xc0\x08\x00"
|
||||||
|
+ context = dns._DecodeContext(maxJumps=3)
|
||||||
|
+ self.assertRaises(
|
||||||
|
+ dns.DNSDecodeError,
|
||||||
|
+ dns.Name().decode,
|
||||||
|
+ BytesIO(payload),
|
||||||
|
+ None,
|
||||||
|
+ context,
|
||||||
|
+ )
|
||||||
|
+
|
||||||
|
+ def test_compressionPointerCounterIsShared(self):
|
||||||
|
+ """
|
||||||
|
+ The L{dns._DecodeContext} counter accumulates across successive
|
||||||
|
+ L{Name.decode} calls, so that a message whose individual names are
|
||||||
|
+ each within bounds is still rejected when their aggregate exceeds
|
||||||
|
+ the configured limit.
|
||||||
|
+ """
|
||||||
|
+ payload = b"\xc0\x02\xc0\x04\x00"
|
||||||
|
+ context = dns._DecodeContext(maxJumps=3)
|
||||||
|
+
|
||||||
|
+ stream = BytesIO(payload)
|
||||||
|
+ dns.Name().decode(stream, context=context)
|
||||||
|
+ self.assertEqual(context.jumps, 2)
|
||||||
|
+
|
||||||
|
+ stream.seek(0)
|
||||||
|
+ self.assertRaises(
|
||||||
|
+ dns.DNSDecodeError,
|
||||||
|
+ dns.Name().decode,
|
||||||
|
+ stream,
|
||||||
|
+ None,
|
||||||
|
+ context,
|
||||||
|
+ )
|
||||||
|
+
|
||||||
|
+ def test_decodeWithoutContextIsBackwardsCompatible(self):
|
||||||
|
+ """
|
||||||
|
+ L{Name.decode} continues to work when called without a context,
|
||||||
|
+ using a fresh per-call counter so existing callers are unaffected.
|
||||||
|
+ """
|
||||||
|
+ name = dns.Name()
|
||||||
|
+ stream = BytesIO()
|
||||||
|
+ dns.Name(b"example.org").encode(stream)
|
||||||
|
+ stream.seek(0)
|
||||||
|
+ name.decode(stream)
|
||||||
|
+ self.assertEqual(name.name, b"example.org")
|
||||||
|
+
|
||||||
|
def test_equality(self):
|
||||||
|
"""
|
||||||
|
L{Name} instances are equal as long as they have the same value for
|
||||||
|
@@ -761,6 +815,43 @@ class MessageTests(unittest.SynchronousTestCase):
|
||||||
|
"""
|
||||||
|
self.assertEqual(dns.Message().authenticData, 0)
|
||||||
|
|
||||||
|
+ def test_rejectCompressionPointerFlood(self):
|
||||||
|
+ """
|
||||||
|
+ L{Message.decode} installs a shared compression-pointer counter and
|
||||||
|
+ raises L{dns.DNSDecodeError} when the aggregate number of pointer
|
||||||
|
+ dereferences across every record in the message exceeds
|
||||||
|
+ L{dns.MAX_COMPRESSION_POINTERS_PER_MESSAGE}.
|
||||||
|
+ """
|
||||||
|
+ chainLength = 100
|
||||||
|
+ numRecords = 8000
|
||||||
|
+ header = struct.pack(
|
||||||
|
+ "!H2B4H", 0x1234, 0x80, 0x00, 0, numRecords, 0, 0
|
||||||
|
+ )
|
||||||
|
+
|
||||||
|
+ # Long compression chain inside the RDATA of an unknown
|
||||||
|
+ # record so that subsequent records can aim pointers at it.
|
||||||
|
+ owner = b"\x04rrrr\x00"
|
||||||
|
+ chainBase = len(header) + len(owner) + 10
|
||||||
|
+ chain = bytearray()
|
||||||
|
+ for i in range(chainLength):
|
||||||
|
+ chain += struct.pack("!H", 0xC000 | (chainBase + 2 * (i + 1)))
|
||||||
|
+ chain += b"\x04test\x00"
|
||||||
|
+
|
||||||
|
+ firstRecord = (
|
||||||
|
+ owner
|
||||||
|
+ + struct.pack("!HHIH", 999, 1, 0, len(chain))
|
||||||
|
+ + bytes(chain)
|
||||||
|
+ )
|
||||||
|
+ followupRecord = (
|
||||||
|
+ struct.pack("!H", 0xC000 | chainBase)
|
||||||
|
+ + struct.pack("!HHIH", 1, 1, 0, 4)
|
||||||
|
+ + b"\x00\x00\x00\x00"
|
||||||
|
+ )
|
||||||
|
+ payload = header + firstRecord + followupRecord * (numRecords - 1)
|
||||||
|
+
|
||||||
|
+ message = dns.Message()
|
||||||
|
+ self.assertRaises(dns.DNSDecodeError, message.decode, BytesIO(payload))
|
||||||
|
+
|
||||||
|
def test_authenticDataOverride(self):
|
||||||
|
"""
|
||||||
|
L{dns.Message.__init__} accepts a C{authenticData} argument which
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
From 094d33d4073368ab1a85fcf63e3b75addb01fd6a Mon Sep 17 00:00:00 2001
|
||||||
|
From: Tomas Illuminati Balbin <dev@tomasilluminati.com>
|
||||||
|
Date: Mon, 20 Apr 2026 09:17:13 -0300
|
||||||
|
Subject: [PATCH] Update src/twisted/names/dns.py
|
||||||
|
|
||||||
|
Co-authored-by: Adi Roiban <adiroiban@gmail.com>
|
||||||
|
|
||||||
|
CVE: CVE-2026-42304
|
||||||
|
Upstream-Status: Backport [https://github.com/twisted/twisted/commit/86e1b5490de5baa8ca284d1e6f4f0ade3e8ad7e0]
|
||||||
|
|
||||||
|
(cherry picked from commit 86e1b5490de5baa8ca284d1e6f4f0ade3e8ad7e0)
|
||||||
|
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||||
|
---
|
||||||
|
src/twisted/names/dns.py | 3 +--
|
||||||
|
1 file changed, 1 insertion(+), 2 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/twisted/names/dns.py b/src/twisted/names/dns.py
|
||||||
|
index 7142d9e75..93e4080bf 100644
|
||||||
|
--- a/src/twisted/names/dns.py
|
||||||
|
+++ b/src/twisted/names/dns.py
|
||||||
|
@@ -2784,8 +2784,7 @@ class Message(tputil.FancyEqMixin):
|
||||||
|
# performed across every name in this message. It is installed on
|
||||||
|
# the context variable so nested record decoders pick it up without
|
||||||
|
# needing to thread it through each signature.
|
||||||
|
- token = _decodeContextVar.set(_DecodeContext())
|
||||||
|
- try:
|
||||||
|
+ with _decodeContextVar.set(_DecodeContext()):
|
||||||
|
self.queries = []
|
||||||
|
for i in range(nqueries):
|
||||||
|
q = Query()
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
From 7213b262b9c98fe6d522dbd23fd83bb9535b54c6 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Tomas Illuminati Balbin <dev@tomasilluminati.com>
|
||||||
|
Date: Mon, 20 Apr 2026 09:18:05 -0300
|
||||||
|
Subject: [PATCH] Update src/twisted/names/test/test_dns.py
|
||||||
|
|
||||||
|
Co-authored-by: Adi Roiban <adiroiban@gmail.com>
|
||||||
|
|
||||||
|
CVE: CVE-2026-42304
|
||||||
|
Upstream-Status: Backport [https://github.com/twisted/twisted/commit/c75d44ed81b47f8086ed801cfcdb2568b0b32301]
|
||||||
|
|
||||||
|
(cherry picked from commit c75d44ed81b47f8086ed801cfcdb2568b0b32301)
|
||||||
|
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||||
|
---
|
||||||
|
src/twisted/names/test/test_dns.py | 6 +++---
|
||||||
|
1 file changed, 3 insertions(+), 3 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/twisted/names/test/test_dns.py b/src/twisted/names/test/test_dns.py
|
||||||
|
index 9b27c4b3a..94aa4a802 100644
|
||||||
|
--- a/src/twisted/names/test/test_dns.py
|
||||||
|
+++ b/src/twisted/names/test/test_dns.py
|
||||||
|
@@ -389,9 +389,9 @@ class NameTests(unittest.TestCase):
|
||||||
|
self.assertRaises(
|
||||||
|
dns.DNSDecodeError,
|
||||||
|
dns.Name().decode,
|
||||||
|
- stream,
|
||||||
|
- None,
|
||||||
|
- context,
|
||||||
|
+ strio=stream,
|
||||||
|
+ length=None,
|
||||||
|
+ context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_decodeWithoutContextIsBackwardsCompatible(self):
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
From 3cb501679cd7f45ab49d32cf72ec546ef3a64825 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Tomas Illuminati <tomas.illuminati@owasp.org>
|
||||||
|
Date: Mon, 20 Apr 2026 10:01:39 -0300
|
||||||
|
Subject: [PATCH] names: Refactor DNS compression mitigation
|
||||||
|
|
||||||
|
CVE: CVE-2026-42304
|
||||||
|
Upstream-Status: Backport [https://github.com/twisted/twisted/commit/d7d81e08d46b3f266963ea77e5f6b4a333af455f]
|
||||||
|
|
||||||
|
Backport Changes:
|
||||||
|
- Adapted the 25.5.0 imports by moving Sequence from typing to
|
||||||
|
collections.abc and retaining the target's Optional and Union imports.
|
||||||
|
|
||||||
|
(cherry picked from commit d7d81e08d46b3f266963ea77e5f6b4a333af455f)
|
||||||
|
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||||
|
---
|
||||||
|
src/twisted/names/dns.py | 113 +++++++++++++------
|
||||||
|
src/twisted/names/newsfragments/12626.bugfix | 1 +
|
||||||
|
src/twisted/names/test/test_dns.py | 47 ++++----
|
||||||
|
3 files changed, 105 insertions(+), 56 deletions(-)
|
||||||
|
create mode 100644 src/twisted/names/newsfragments/12626.bugfix
|
||||||
|
|
||||||
|
diff --git a/src/twisted/names/dns.py b/src/twisted/names/dns.py
|
||||||
|
index 93e4080bf..869ffec76 100644
|
||||||
|
--- a/src/twisted/names/dns.py
|
||||||
|
+++ b/src/twisted/names/dns.py
|
||||||
|
@@ -16,9 +16,11 @@ import inspect
|
||||||
|
import random
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
+from collections.abc import Sequence
|
||||||
|
+from contextlib import contextmanager
|
||||||
|
from io import BytesIO
|
||||||
|
from itertools import chain
|
||||||
|
-from typing import Optional, Sequence, SupportsInt, Union, overload
|
||||||
|
+from typing import Final, Optional, SupportsInt, Union, overload
|
||||||
|
|
||||||
|
from zope.interface import Attribute, Interface, implementer
|
||||||
|
|
||||||
|
@@ -446,17 +448,19 @@ def readPrecisely(file, l):
|
||||||
|
return buff
|
||||||
|
|
||||||
|
|
||||||
|
-# Cap the total number of compression-pointer dereferences performed while
|
||||||
|
-# decoding a single DNS message. A hostile peer can otherwise craft a packet
|
||||||
|
-# in which every record name chases a long compression chain, forcing O(N*M)
|
||||||
|
-# work and stalling the reactor.
|
||||||
|
-MAX_COMPRESSION_POINTERS_PER_MESSAGE = 1000
|
||||||
|
+MAX_COMPRESSION_POINTERS_PER_MESSAGE: Final = 1000
|
||||||
|
+"""
|
||||||
|
+Cap the total number of compression-pointer dereferences performed while
|
||||||
|
+decoding a single DNS message. A hostile peer can otherwise craft a packet
|
||||||
|
+in which every record name chases a long compression chain, forcing
|
||||||
|
+C{O(N*M)} work and stalling the reactor.
|
||||||
|
+"""
|
||||||
|
|
||||||
|
|
||||||
|
class DNSDecodeError(ValueError):
|
||||||
|
"""
|
||||||
|
Raised when a DNS message cannot be decoded because it violates a
|
||||||
|
- protocol-level safety limit
|
||||||
|
+ protocol-level safety limit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@@ -469,8 +473,12 @@ class _DecodeContext:
|
||||||
|
jumps taken across every name in the message, defending against packets
|
||||||
|
that fan out thousands of records pointing to deeply chained pointers.
|
||||||
|
|
||||||
|
+ This class is private. External callers must not rely on it; the
|
||||||
|
+ per-message scope is installed and torn down by L{Message.decode}
|
||||||
|
+ through L{_decodeContextVar}.
|
||||||
|
+
|
||||||
|
@ivar jumps: The number of compression pointers followed so far.
|
||||||
|
- @ivar maxJumps: The inclusive upper bound on C{jumps}. Exceeding it
|
||||||
|
+ @ivar maxJumps: The inclusive upper bound on L{jumps}. Exceeding it
|
||||||
|
causes L{registerJump} to raise L{DNSDecodeError}.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@@ -482,10 +490,14 @@ class _DecodeContext:
|
||||||
|
|
||||||
|
def registerJump(self) -> None:
|
||||||
|
"""
|
||||||
|
- Record that a compression pointer has been followed
|
||||||
|
+ Record that a compression pointer has been followed.
|
||||||
|
+
|
||||||
|
+ The check is performed before any further bytes are read so the
|
||||||
|
+ caller fails fast as soon as the aggregate limit is breached, even
|
||||||
|
+ if additional records remain in the buffer.
|
||||||
|
|
||||||
|
@raise DNSDecodeError: if the cumulative number of jumps exceeds
|
||||||
|
- L{maxJumps}
|
||||||
|
+ L{maxJumps}.
|
||||||
|
"""
|
||||||
|
self.jumps += 1
|
||||||
|
if self.jumps > self.maxJumps:
|
||||||
|
@@ -495,15 +507,37 @@ class _DecodeContext:
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
-# Tracks state across nested calls without altering every record's signature.
|
||||||
|
-# L{Message.decode} manages the lifecycle per-message, while standalone decoders
|
||||||
|
-# default to a local context when C{_decodeContextVar} is C{None}
|
||||||
|
-
|
||||||
|
+# Private module-level L{contextvars.ContextVar} used to share a single
|
||||||
|
+# L{_DecodeContext} across the re-entrant calls performed while decoding one
|
||||||
|
+# DNS message. L{contextvars} (rather than a plain module attribute) is used
|
||||||
|
+# on purpose: although Twisted's reactor is single-threaded, message decoding
|
||||||
|
+# is re-entrant across many records in a single pass and L{ContextVar}
|
||||||
|
+# guarantees the scope is restored correctly on exit -- and remains isolated
|
||||||
|
+# per-task should a future caller decode messages from multiple
|
||||||
|
+# L{asyncio}-style contexts concurrently.
|
||||||
|
_decodeContextVar: contextvars.ContextVar[_DecodeContext | None] = (
|
||||||
|
contextvars.ContextVar("_dnsDecodeContext", default=None)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
+@contextmanager
|
||||||
|
+def _installDecodeContext(context: _DecodeContext):
|
||||||
|
+ """
|
||||||
|
+ Install C{context} on L{_decodeContextVar} for the duration of the
|
||||||
|
+ C{with} block and restore the previous value on exit.
|
||||||
|
+
|
||||||
|
+ This wraps the L{contextvars.ContextVar.set} / L{contextvars.ContextVar.reset}
|
||||||
|
+ token dance so call sites can use a plain C{with} statement.
|
||||||
|
+
|
||||||
|
+ @param context: The L{_DecodeContext} to install as the active context.
|
||||||
|
+ """
|
||||||
|
+ token = _decodeContextVar.set(context)
|
||||||
|
+ try:
|
||||||
|
+ yield context
|
||||||
|
+ finally:
|
||||||
|
+ _decodeContextVar.reset(token)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
class IEncodable(Interface):
|
||||||
|
"""
|
||||||
|
Interface for something which can be encoded to and decoded
|
||||||
|
@@ -609,8 +643,18 @@ class Name:
|
||||||
|
|
||||||
|
@ivar name: A byte string giving the name.
|
||||||
|
@type name: L{bytes}
|
||||||
|
+
|
||||||
|
+ @cvar maxCompressionPointers: Per-message cap on the total number of
|
||||||
|
+ compression-pointer dereferences L{decode} will follow before
|
||||||
|
+ raising L{DNSDecodeError}. Defined as a class attribute so
|
||||||
|
+ subclasses (and, in the future, individual instances) may override
|
||||||
|
+ it to tune the trade-off between tolerance for legitimately
|
||||||
|
+ verbose messages and resistance to denial-of-service attacks.
|
||||||
|
+ @type maxCompressionPointers: L{int}
|
||||||
|
"""
|
||||||
|
|
||||||
|
+ maxCompressionPointers: int = MAX_COMPRESSION_POINTERS_PER_MESSAGE
|
||||||
|
+
|
||||||
|
def __init__(self, name: bytes | str = b""):
|
||||||
|
"""
|
||||||
|
@param name: A name.
|
||||||
|
@@ -651,35 +695,37 @@ class Name:
|
||||||
|
strio.write(label)
|
||||||
|
strio.write(b"\x00")
|
||||||
|
|
||||||
|
- def decode(self, strio, length=None, context=None):
|
||||||
|
+ def decode(self, strio, length=None):
|
||||||
|
"""
|
||||||
|
Decode a byte string into this Name.
|
||||||
|
|
||||||
|
+ When invoked from L{Message.decode}, a shared compression-pointer
|
||||||
|
+ counter is picked up transparently from the private
|
||||||
|
+ L{_decodeContextVar}. Standalone callers get a fresh per-call
|
||||||
|
+ counter seeded from L{maxCompressionPointers}, so existing code
|
||||||
|
+ keeps working unchanged while still being protected against
|
||||||
|
+ pathological inputs.
|
||||||
|
+
|
||||||
|
@type strio: file
|
||||||
|
@param strio: Bytes will be read from this file until the full Name
|
||||||
|
- is decoded.
|
||||||
|
+ is decoded.
|
||||||
|
|
||||||
|
- @type context: L{_DecodeContext} or L{None}
|
||||||
|
- @param context: Shared decoding state used to cap the total number
|
||||||
|
- of compression-pointer jumps taken while decoding the enclosing
|
||||||
|
- DNS message. When L{None}, the context installed by
|
||||||
|
- L{Message.decode} is used if one is active; otherwise a fresh,
|
||||||
|
- call-local context is created so that direct callers remain
|
||||||
|
- protected and backwards compatible.
|
||||||
|
+ @type length: L{int} or L{None}
|
||||||
|
+ @param length: Present for compatibility with the L{IEncodable}
|
||||||
|
+ interface; ignored by this decoder.
|
||||||
|
|
||||||
|
@raise EOFError: Raised when there are not enough bytes available
|
||||||
|
- from C{strio}.
|
||||||
|
+ from C{strio}.
|
||||||
|
|
||||||
|
- @raise ValueError: Raised when the name cannot be decoded because it
|
||||||
|
- contains a compression loop.
|
||||||
|
+ @raise ValueError: Raised when the name cannot be decoded because
|
||||||
|
+ it contains a compression loop.
|
||||||
|
|
||||||
|
@raise DNSDecodeError: Raised when the cumulative number of
|
||||||
|
compression-pointer jumps exceeds the configured limit.
|
||||||
|
"""
|
||||||
|
+ context = _decodeContextVar.get()
|
||||||
|
if context is None:
|
||||||
|
- context = _decodeContextVar.get()
|
||||||
|
- if context is None:
|
||||||
|
- context = _DecodeContext()
|
||||||
|
+ context = _DecodeContext(maxJumps=self.maxCompressionPointers)
|
||||||
|
visited = set()
|
||||||
|
self.name = b""
|
||||||
|
off = 0
|
||||||
|
@@ -2782,9 +2828,10 @@ class Message(tputil.FancyEqMixin):
|
||||||
|
|
||||||
|
# A single shared counter bounds the total compression-pointer work
|
||||||
|
# performed across every name in this message. It is installed on
|
||||||
|
- # the context variable so nested record decoders pick it up without
|
||||||
|
- # needing to thread it through each signature.
|
||||||
|
- with _decodeContextVar.set(_DecodeContext()):
|
||||||
|
+ # the private context variable so nested record decoders pick it up
|
||||||
|
+ # without needing to thread it through each signature.
|
||||||
|
+ decodeContext = _DecodeContext(maxJumps=Name.maxCompressionPointers)
|
||||||
|
+ with _installDecodeContext(decodeContext):
|
||||||
|
self.queries = []
|
||||||
|
for i in range(nqueries):
|
||||||
|
q = Query()
|
||||||
|
@@ -2802,8 +2849,6 @@ class Message(tputil.FancyEqMixin):
|
||||||
|
|
||||||
|
for l, n in items:
|
||||||
|
self.parseRecords(l, n, strio)
|
||||||
|
- finally:
|
||||||
|
- _decodeContextVar.reset(token)
|
||||||
|
|
||||||
|
def parseRecords(self, list, num, strio):
|
||||||
|
for i in range(num):
|
||||||
|
diff --git a/src/twisted/names/newsfragments/12626.bugfix b/src/twisted/names/newsfragments/12626.bugfix
|
||||||
|
new file mode 100644
|
||||||
|
index 000000000..44896c3e5
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/src/twisted/names/newsfragments/12626.bugfix
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+twisted.names was fix for Denial of Service (DoS) attack via resource exhaustion during DNS name decompression. CVE-REFERENCE HERE
|
||||||
|
\ No newline at end of file
|
||||||
|
diff --git a/src/twisted/names/test/test_dns.py b/src/twisted/names/test/test_dns.py
|
||||||
|
index 94aa4a802..9626115ab 100644
|
||||||
|
--- a/src/twisted/names/test/test_dns.py
|
||||||
|
+++ b/src/twisted/names/test/test_dns.py
|
||||||
|
@@ -356,48 +356,51 @@ class NameTests(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
L{Name.decode} raises L{dns.DNSDecodeError} when the number of
|
||||||
|
compression-pointer dereferences taken for a single message exceeds
|
||||||
|
- the limit carried by the shared L{dns._DecodeContext}.
|
||||||
|
+ the limit carried by the shared L{dns._DecodeContext} installed
|
||||||
|
+ through the private L{dns._decodeContextVar}.
|
||||||
|
"""
|
||||||
|
# Five distinct pointers chained end-to-end, terminated by a zero
|
||||||
|
# label byte. With a maxJumps of three the fourth dereference must
|
||||||
|
# trip the safety limit.
|
||||||
|
payload = b"\xc0\x02\xc0\x04\xc0\x06\xc0\x08\x00"
|
||||||
|
context = dns._DecodeContext(maxJumps=3)
|
||||||
|
- self.assertRaises(
|
||||||
|
- dns.DNSDecodeError,
|
||||||
|
- dns.Name().decode,
|
||||||
|
- BytesIO(payload),
|
||||||
|
- None,
|
||||||
|
- context,
|
||||||
|
- )
|
||||||
|
+ with dns._installDecodeContext(context):
|
||||||
|
+ self.assertRaises(
|
||||||
|
+ dns.DNSDecodeError,
|
||||||
|
+ dns.Name().decode,
|
||||||
|
+ BytesIO(payload),
|
||||||
|
+ )
|
||||||
|
|
||||||
|
def test_compressionPointerCounterIsShared(self):
|
||||||
|
"""
|
||||||
|
The L{dns._DecodeContext} counter accumulates across successive
|
||||||
|
L{Name.decode} calls, so that a message whose individual names are
|
||||||
|
each within bounds is still rejected when their aggregate exceeds
|
||||||
|
- the configured limit.
|
||||||
|
+ the configured limit. This mirrors production: L{Message.decode}
|
||||||
|
+ invokes L{Name.decode} many times against the same stream under one
|
||||||
|
+ shared context.
|
||||||
|
"""
|
||||||
|
payload = b"\xc0\x02\xc0\x04\x00"
|
||||||
|
context = dns._DecodeContext(maxJumps=3)
|
||||||
|
|
||||||
|
- stream = BytesIO(payload)
|
||||||
|
- dns.Name().decode(stream, context=context)
|
||||||
|
- self.assertEqual(context.jumps, 2)
|
||||||
|
+ with dns._installDecodeContext(context):
|
||||||
|
+ stream = BytesIO(payload)
|
||||||
|
+ dns.Name().decode(stream)
|
||||||
|
+ self.assertEqual(context.jumps, 2)
|
||||||
|
|
||||||
|
- stream.seek(0)
|
||||||
|
- self.assertRaises(
|
||||||
|
- dns.DNSDecodeError,
|
||||||
|
- dns.Name().decode,
|
||||||
|
- strio=stream,
|
||||||
|
- length=None,
|
||||||
|
- context=context,
|
||||||
|
- )
|
||||||
|
+ stream.seek(0)
|
||||||
|
+ self.assertRaises(
|
||||||
|
+ dns.DNSDecodeError,
|
||||||
|
+ dns.Name().decode,
|
||||||
|
+ stream,
|
||||||
|
+ )
|
||||||
|
|
||||||
|
def test_decodeWithoutContextIsBackwardsCompatible(self):
|
||||||
|
"""
|
||||||
|
- L{Name.decode} continues to work when called without a context,
|
||||||
|
- using a fresh per-call counter so existing callers are unaffected.
|
||||||
|
+ L{Name.decode} continues to work when called with no active
|
||||||
|
+ L{dns._decodeContextVar}, using a fresh per-call counter seeded
|
||||||
|
+ from L{dns.Name.maxCompressionPointers} so existing callers are
|
||||||
|
+ unaffected.
|
||||||
|
"""
|
||||||
|
name = dns.Name()
|
||||||
|
stream = BytesIO()
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
From 4aa0fdb33f2a12db2d234754b9b03d74df8eaf9d Mon Sep 17 00:00:00 2001
|
||||||
|
From: Tomas Illuminati <tomas.illuminati@owasp.org>
|
||||||
|
Date: Tue, 21 Apr 2026 17:26:49 -0300
|
||||||
|
Subject: [PATCH] names: fix changes
|
||||||
|
|
||||||
|
CVE: CVE-2026-42304
|
||||||
|
Upstream-Status: Backport [https://github.com/twisted/twisted/commit/9df6d960d3569751ebb5567093fe1d1d9f63ca54]
|
||||||
|
|
||||||
|
Backport Changes:
|
||||||
|
- Retained the 25.5.0 Optional and Union typing imports while removing
|
||||||
|
Final; Sequence remains sourced from collections.abc after p4.
|
||||||
|
|
||||||
|
(cherry picked from commit 9df6d960d3569751ebb5567093fe1d1d9f63ca54)
|
||||||
|
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||||
|
---
|
||||||
|
src/twisted/names/dns.py | 37 +++++++------
|
||||||
|
src/twisted/names/test/test_dns.py | 85 +++++++++++++-----------------
|
||||||
|
2 files changed, 56 insertions(+), 66 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/twisted/names/dns.py b/src/twisted/names/dns.py
|
||||||
|
index 869ffec76..ca5079454 100644
|
||||||
|
--- a/src/twisted/names/dns.py
|
||||||
|
+++ b/src/twisted/names/dns.py
|
||||||
|
@@ -20,7 +20,7 @@ from collections.abc import Sequence
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from io import BytesIO
|
||||||
|
from itertools import chain
|
||||||
|
-from typing import Final, Optional, SupportsInt, Union, overload
|
||||||
|
+from typing import Optional, SupportsInt, Union, overload
|
||||||
|
|
||||||
|
from zope.interface import Attribute, Interface, implementer
|
||||||
|
|
||||||
|
@@ -448,15 +448,6 @@ def readPrecisely(file, l):
|
||||||
|
return buff
|
||||||
|
|
||||||
|
|
||||||
|
-MAX_COMPRESSION_POINTERS_PER_MESSAGE: Final = 1000
|
||||||
|
-"""
|
||||||
|
-Cap the total number of compression-pointer dereferences performed while
|
||||||
|
-decoding a single DNS message. A hostile peer can otherwise craft a packet
|
||||||
|
-in which every record name chases a long compression chain, forcing
|
||||||
|
-C{O(N*M)} work and stalling the reactor.
|
||||||
|
-"""
|
||||||
|
-
|
||||||
|
-
|
||||||
|
class DNSDecodeError(ValueError):
|
||||||
|
"""
|
||||||
|
Raised when a DNS message cannot be decoded because it violates a
|
||||||
|
@@ -484,7 +475,7 @@ class _DecodeContext:
|
||||||
|
|
||||||
|
__slots__ = ("jumps", "maxJumps")
|
||||||
|
|
||||||
|
- def __init__(self, maxJumps: int = MAX_COMPRESSION_POINTERS_PER_MESSAGE) -> None:
|
||||||
|
+ def __init__(self, maxJumps: int = 1000) -> None:
|
||||||
|
self.jumps = 0
|
||||||
|
self.maxJumps = maxJumps
|
||||||
|
|
||||||
|
@@ -644,16 +635,15 @@ class Name:
|
||||||
|
@ivar name: A byte string giving the name.
|
||||||
|
@type name: L{bytes}
|
||||||
|
|
||||||
|
- @cvar maxCompressionPointers: Per-message cap on the total number of
|
||||||
|
+ @ivar maxCompressionPointers: Per-message cap on the total number of
|
||||||
|
compression-pointer dereferences L{decode} will follow before
|
||||||
|
- raising L{DNSDecodeError}. Defined as a class attribute so
|
||||||
|
- subclasses (and, in the future, individual instances) may override
|
||||||
|
- it to tune the trade-off between tolerance for legitimately
|
||||||
|
- verbose messages and resistance to denial-of-service attacks.
|
||||||
|
- @type maxCompressionPointers: L{int}
|
||||||
|
+ raising L{DNSDecodeError}. Defaults to C{1000}. Override it on
|
||||||
|
+ a subclass or individual instance to tune the trade-off between
|
||||||
|
+ tolerance for legitimately verbose messages and resistance to
|
||||||
|
+ denial-of-service attacks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
- maxCompressionPointers: int = MAX_COMPRESSION_POINTERS_PER_MESSAGE
|
||||||
|
+ maxCompressionPointers: int = 1000
|
||||||
|
|
||||||
|
def __init__(self, name: bytes | str = b""):
|
||||||
|
"""
|
||||||
|
@@ -2610,8 +2600,17 @@ class Message(tputil.FancyEqMixin):
|
||||||
|
header fields.
|
||||||
|
@ivar _sectionNames: The names of attributes representing the record
|
||||||
|
sections of this message.
|
||||||
|
+
|
||||||
|
+ @ivar maxCompressionPointers: Per-message cap on the total number of
|
||||||
|
+ compression-pointer dereferences L{decode} will follow across every
|
||||||
|
+ name in the message before raising L{DNSDecodeError}. Defaults to
|
||||||
|
+ C{1000}. Override it on a subclass or individual instance to tune
|
||||||
|
+ the trade-off between tolerance for legitimately verbose messages
|
||||||
|
+ and resistance to denial-of-service attacks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
+ maxCompressionPointers: int = 1000
|
||||||
|
+
|
||||||
|
compareAttributes = (
|
||||||
|
"id",
|
||||||
|
"answer",
|
||||||
|
@@ -2830,7 +2829,7 @@ class Message(tputil.FancyEqMixin):
|
||||||
|
# performed across every name in this message. It is installed on
|
||||||
|
# the private context variable so nested record decoders pick it up
|
||||||
|
# without needing to thread it through each signature.
|
||||||
|
- decodeContext = _DecodeContext(maxJumps=Name.maxCompressionPointers)
|
||||||
|
+ decodeContext = _DecodeContext(maxJumps=self.maxCompressionPointers)
|
||||||
|
with _installDecodeContext(decodeContext):
|
||||||
|
self.queries = []
|
||||||
|
for i in range(nqueries):
|
||||||
|
diff --git a/src/twisted/names/test/test_dns.py b/src/twisted/names/test/test_dns.py
|
||||||
|
index 9626115ab..3be6b4546 100644
|
||||||
|
--- a/src/twisted/names/test/test_dns.py
|
||||||
|
+++ b/src/twisted/names/test/test_dns.py
|
||||||
|
@@ -354,60 +354,51 @@ class NameTests(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_rejectTooManyCompressionPointers(self):
|
||||||
|
"""
|
||||||
|
- L{Name.decode} raises L{dns.DNSDecodeError} when the number of
|
||||||
|
- compression-pointer dereferences taken for a single message exceeds
|
||||||
|
- the limit carried by the shared L{dns._DecodeContext} installed
|
||||||
|
- through the private L{dns._decodeContextVar}.
|
||||||
|
- """
|
||||||
|
- # Five distinct pointers chained end-to-end, terminated by a zero
|
||||||
|
- # label byte. With a maxJumps of three the fourth dereference must
|
||||||
|
- # trip the safety limit.
|
||||||
|
+ L{Name.decode} raises L{dns.DNSDecodeError} when it would have to
|
||||||
|
+ follow more than L{Name.maxCompressionPointers} compression
|
||||||
|
+ pointers to finish decoding a name.
|
||||||
|
+ """
|
||||||
|
+ # Four distinct pointers chained end-to-end, terminated by a zero
|
||||||
|
+ # label byte. With maxCompressionPointers of three the fourth
|
||||||
|
+ # dereference must trip the safety limit.
|
||||||
|
payload = b"\xc0\x02\xc0\x04\xc0\x06\xc0\x08\x00"
|
||||||
|
- context = dns._DecodeContext(maxJumps=3)
|
||||||
|
- with dns._installDecodeContext(context):
|
||||||
|
- self.assertRaises(
|
||||||
|
- dns.DNSDecodeError,
|
||||||
|
- dns.Name().decode,
|
||||||
|
- BytesIO(payload),
|
||||||
|
- )
|
||||||
|
+ name = dns.Name()
|
||||||
|
+ name.maxCompressionPointers = 3
|
||||||
|
+ self.assertRaises(
|
||||||
|
+ dns.DNSDecodeError, name.decode, BytesIO(payload)
|
||||||
|
+ )
|
||||||
|
|
||||||
|
- def test_compressionPointerCounterIsShared(self):
|
||||||
|
+ def test_decodeRecoversAfterDNSDecodeError(self):
|
||||||
|
"""
|
||||||
|
- The L{dns._DecodeContext} counter accumulates across successive
|
||||||
|
- L{Name.decode} calls, so that a message whose individual names are
|
||||||
|
- each within bounds is still rejected when their aggregate exceeds
|
||||||
|
- the configured limit. This mirrors production: L{Message.decode}
|
||||||
|
- invokes L{Name.decode} many times against the same stream under one
|
||||||
|
- shared context.
|
||||||
|
+ After L{Name.decode} raises L{dns.DNSDecodeError}, subsequent
|
||||||
|
+ L{Name.decode} calls continue to work. No residual
|
||||||
|
+ compression-pointer counter leaks across calls, so a legitimate
|
||||||
|
+ name decoded right after a hostile one still succeeds.
|
||||||
|
"""
|
||||||
|
- payload = b"\xc0\x02\xc0\x04\x00"
|
||||||
|
- context = dns._DecodeContext(maxJumps=3)
|
||||||
|
-
|
||||||
|
- with dns._installDecodeContext(context):
|
||||||
|
- stream = BytesIO(payload)
|
||||||
|
- dns.Name().decode(stream)
|
||||||
|
- self.assertEqual(context.jumps, 2)
|
||||||
|
-
|
||||||
|
- stream.seek(0)
|
||||||
|
- self.assertRaises(
|
||||||
|
- dns.DNSDecodeError,
|
||||||
|
- dns.Name().decode,
|
||||||
|
- stream,
|
||||||
|
- )
|
||||||
|
+ # First, force a DNSDecodeError by decoding a payload that
|
||||||
|
+ # exceeds the configured limit.
|
||||||
|
+ hostile = dns.Name()
|
||||||
|
+ hostile.maxCompressionPointers = 3
|
||||||
|
+ self.assertRaises(
|
||||||
|
+ dns.DNSDecodeError,
|
||||||
|
+ hostile.decode,
|
||||||
|
+ BytesIO(b"\xc0\x02\xc0\x04\xc0\x06\xc0\x08\x00"),
|
||||||
|
+ )
|
||||||
|
|
||||||
|
- def test_decodeWithoutContextIsBackwardsCompatible(self):
|
||||||
|
- """
|
||||||
|
- L{Name.decode} continues to work when called with no active
|
||||||
|
- L{dns._decodeContextVar}, using a fresh per-call counter seeded
|
||||||
|
- from L{dns.Name.maxCompressionPointers} so existing callers are
|
||||||
|
- unaffected.
|
||||||
|
- """
|
||||||
|
- name = dns.Name()
|
||||||
|
+ # Then prove the process has not been poisoned: a legitimate
|
||||||
|
+ # name still decodes normally, both with a fresh instance and
|
||||||
|
+ # with the instance that just errored.
|
||||||
|
stream = BytesIO()
|
||||||
|
dns.Name(b"example.org").encode(stream)
|
||||||
|
+
|
||||||
|
+ fresh = dns.Name()
|
||||||
|
stream.seek(0)
|
||||||
|
- name.decode(stream)
|
||||||
|
- self.assertEqual(name.name, b"example.org")
|
||||||
|
+ fresh.decode(stream)
|
||||||
|
+ self.assertEqual(fresh.name, b"example.org")
|
||||||
|
+
|
||||||
|
+ stream.seek(0)
|
||||||
|
+ hostile.decode(stream)
|
||||||
|
+ self.assertEqual(hostile.name, b"example.org")
|
||||||
|
|
||||||
|
def test_equality(self):
|
||||||
|
"""
|
||||||
|
@@ -823,7 +814,7 @@ class MessageTests(unittest.SynchronousTestCase):
|
||||||
|
L{Message.decode} installs a shared compression-pointer counter and
|
||||||
|
raises L{dns.DNSDecodeError} when the aggregate number of pointer
|
||||||
|
dereferences across every record in the message exceeds
|
||||||
|
- L{dns.MAX_COMPRESSION_POINTERS_PER_MESSAGE}.
|
||||||
|
+ L{dns.Message.maxCompressionPointers}.
|
||||||
|
"""
|
||||||
|
chainLength = 100
|
||||||
|
numRecords = 8000
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
From 2dc37caa8af3559e14fc1a1b36b049851d36943c Mon Sep 17 00:00:00 2001
|
||||||
|
From: Adi Roiban <adiroiban@gmail.com>
|
||||||
|
Date: Wed, 29 Apr 2026 15:55:05 +0100
|
||||||
|
Subject: [PATCH] Update src/twisted/names/newsfragments/12626.bugfix
|
||||||
|
|
||||||
|
CVE: CVE-2026-42304
|
||||||
|
Upstream-Status: Backport [https://github.com/twisted/twisted/commit/9ca319ebf61386dd33354c4ade3946ef84ad58fb]
|
||||||
|
|
||||||
|
(cherry picked from commit 9ca319ebf61386dd33354c4ade3946ef84ad58fb)
|
||||||
|
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||||
|
---
|
||||||
|
src/twisted/names/newsfragments/12626.bugfix | 2 +-
|
||||||
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/src/twisted/names/newsfragments/12626.bugfix b/src/twisted/names/newsfragments/12626.bugfix
|
||||||
|
index 44896c3e5..179b92d83 100644
|
||||||
|
--- a/src/twisted/names/newsfragments/12626.bugfix
|
||||||
|
+++ b/src/twisted/names/newsfragments/12626.bugfix
|
||||||
|
@@ -1 +1 @@
|
||||||
|
-twisted.names was fix for Denial of Service (DoS) attack via resource exhaustion during DNS name decompression. CVE-REFERENCE HERE
|
||||||
|
\ No newline at end of file
|
||||||
|
+twisted.names was fix for Denial of Service (DoS) attack via resource exhaustion during DNS name decompression. CVE-2026-42304
|
||||||
|
\ No newline at end of file
|
||||||
@@ -6,6 +6,14 @@ HOMEPAGE = "https://twisted.org"
|
|||||||
LICENSE = "MIT"
|
LICENSE = "MIT"
|
||||||
LIC_FILES_CHKSUM = "file://LICENSE;md5=5316a448a61a38d722c291f78d915d11"
|
LIC_FILES_CHKSUM = "file://LICENSE;md5=5316a448a61a38d722c291f78d915d11"
|
||||||
|
|
||||||
|
SRC_URI += "file://CVE-2026-42304_p1.patch \
|
||||||
|
file://CVE-2026-42304_p2.patch \
|
||||||
|
file://CVE-2026-42304_p3.patch \
|
||||||
|
file://CVE-2026-42304_p4.patch \
|
||||||
|
file://CVE-2026-42304_p5.patch \
|
||||||
|
file://CVE-2026-42304_p6.patch \
|
||||||
|
"
|
||||||
|
|
||||||
SRC_URI[sha256sum] = "1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316"
|
SRC_URI[sha256sum] = "1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316"
|
||||||
|
|
||||||
CVE_PRODUCT = "twisted"
|
CVE_PRODUCT = "twisted"
|
||||||
|
|||||||
Reference in new issue
Block a user