diff --git a/0001-Address-DoS-via-the-Tudoor-mechanism-CVE-2023-29483-.patch b/0001-Address-DoS-via-the-Tudoor-mechanism-CVE-2023-29483-.patch new file mode 100644 index 0000000000000000000000000000000000000000..14faad54384e9b69c5bf9a9cacdc408fb32f63e6 --- /dev/null +++ b/0001-Address-DoS-via-the-Tudoor-mechanism-CVE-2023-29483-.patch @@ -0,0 +1,321 @@ +From f66e25b5f549acf66d1fb6ead13eb3cff7d09af3 Mon Sep 17 00:00:00 2001 +From: Bob Halley +Date: Fri, 9 Feb 2024 11:22:52 -0800 +Subject: [PATCH] Address DoS via the Tudoor mechanism (CVE-2023-29483) (#1044) + +--- + dns/asyncquery.py | 45 +++++++++++++------ + dns/nameserver.py | 2 + + dns/query.py | 110 +++++++++++++++++++++++++++++----------------- + 3 files changed, 103 insertions(+), 54 deletions(-) + +diff --git a/dns/asyncquery.py b/dns/asyncquery.py +index 35a355b..94cb241 100644 +--- a/dns/asyncquery.py ++++ b/dns/asyncquery.py +@@ -120,6 +120,8 @@ async def receive_udp( + request_mac: Optional[bytes] = b"", + ignore_trailing: bool = False, + raise_on_truncation: bool = False, ++ ignore_errors: bool = False, ++ query: Optional[dns.message.Message] = None, + ) -> Any: + """Read a DNS message from a UDP socket. + +@@ -133,22 +135,30 @@ async def receive_udp( + """ + + wire = b"" +- while 1: ++ while True: + (wire, from_address) = await sock.recvfrom(65535, _timeout(expiration)) +- if _matches_destination( ++ if not _matches_destination( + sock.family, from_address, destination, ignore_unexpected + ): +- break +- received_time = time.time() +- r = dns.message.from_wire( +- wire, +- keyring=keyring, +- request_mac=request_mac, +- one_rr_per_rrset=one_rr_per_rrset, +- ignore_trailing=ignore_trailing, +- raise_on_truncation=raise_on_truncation, +- ) +- return (r, received_time, from_address) ++ continue ++ received_time = time.time() ++ try: ++ r = dns.message.from_wire( ++ wire, ++ keyring=keyring, ++ request_mac=request_mac, ++ one_rr_per_rrset=one_rr_per_rrset, ++ ignore_trailing=ignore_trailing, ++ raise_on_truncation=raise_on_truncation, ++ ) ++ except Exception: ++ if ignore_errors: ++ continue ++ else: ++ raise ++ if ignore_errors and query is not None and not query.is_response(r): ++ continue ++ return (r, received_time, from_address) + + + async def udp( +@@ -164,6 +174,7 @@ async def udp( + raise_on_truncation: bool = False, + sock: Optional[dns.asyncbackend.DatagramSocket] = None, + backend: Optional[dns.asyncbackend.Backend] = None, ++ ignore_errors: bool = False, + ) -> dns.message.Message: + """Return the response obtained after sending a query via UDP. + +@@ -205,9 +216,13 @@ async def udp( + q.mac, + ignore_trailing, + raise_on_truncation, ++ ignore_errors, ++ q, + ) + r.time = received_time - begin_time +- if not q.is_response(r): ++ # We don't need to check q.is_response() if we are in ignore_errors mode ++ # as receive_udp() will have checked it. ++ if not (ignore_errors or q.is_response(r)): + raise BadResponse + return r + +@@ -225,6 +240,7 @@ async def udp_with_fallback( + udp_sock: Optional[dns.asyncbackend.DatagramSocket] = None, + tcp_sock: Optional[dns.asyncbackend.StreamSocket] = None, + backend: Optional[dns.asyncbackend.Backend] = None, ++ ignore_errors: bool = False, + ) -> Tuple[dns.message.Message, bool]: + """Return the response to the query, trying UDP first and falling back + to TCP if UDP results in a truncated response. +@@ -260,6 +276,7 @@ async def udp_with_fallback( + True, + udp_sock, + backend, ++ ignore_errors, + ) + return (response, False) + except dns.message.Truncated: +diff --git a/dns/nameserver.py b/dns/nameserver.py +index a1fb549..0c494c1 100644 +--- a/dns/nameserver.py ++++ b/dns/nameserver.py +@@ -115,6 +115,7 @@ class Do53Nameserver(AddressAndPortNameserver): + raise_on_truncation=True, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, ++ ignore_errors=True, + ) + return response + +@@ -153,6 +154,7 @@ class Do53Nameserver(AddressAndPortNameserver): + backend=backend, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, ++ ignore_errors=True, + ) + return response + +diff --git a/dns/query.py b/dns/query.py +index d4bd6b9..bdd251e 100644 +--- a/dns/query.py ++++ b/dns/query.py +@@ -569,6 +569,8 @@ def receive_udp( + request_mac: Optional[bytes] = b"", + ignore_trailing: bool = False, + raise_on_truncation: bool = False, ++ ignore_errors: bool = False, ++ query: Optional[dns.message.Message] = None, + ) -> Any: + """Read a DNS message from a UDP socket. + +@@ -609,28 +611,44 @@ def receive_udp( + ``(dns.message.Message, float, tuple)`` + tuple of the received message, the received time, and the address where + the message arrived from. ++ ++ *ignore_errors*, a ``bool``. If various format errors or response ++ mismatches occur, ignore them and keep listening for a valid response. ++ The default is ``False``. ++ ++ *query*, a ``dns.message.Message`` or ``None``. If not ``None`` and ++ *ignore_errors* is ``True``, check that the received message is a response ++ to this query, and if not keep listening for a valid response. + """ + + wire = b"" + while True: + (wire, from_address) = _udp_recv(sock, 65535, expiration) +- if _matches_destination( ++ if not _matches_destination( + sock.family, from_address, destination, ignore_unexpected + ): +- break +- received_time = time.time() +- r = dns.message.from_wire( +- wire, +- keyring=keyring, +- request_mac=request_mac, +- one_rr_per_rrset=one_rr_per_rrset, +- ignore_trailing=ignore_trailing, +- raise_on_truncation=raise_on_truncation, +- ) +- if destination: +- return (r, received_time) +- else: +- return (r, received_time, from_address) ++ continue ++ received_time = time.time() ++ try: ++ r = dns.message.from_wire( ++ wire, ++ keyring=keyring, ++ request_mac=request_mac, ++ one_rr_per_rrset=one_rr_per_rrset, ++ ignore_trailing=ignore_trailing, ++ raise_on_truncation=raise_on_truncation, ++ ) ++ except Exception: ++ if ignore_errors: ++ continue ++ else: ++ raise ++ if ignore_errors and query is not None and not query.is_response(r): ++ continue ++ if destination: ++ return (r, received_time) ++ else: ++ return (r, received_time, from_address) + + + def udp( +@@ -645,6 +663,7 @@ def udp( + ignore_trailing: bool = False, + raise_on_truncation: bool = False, + sock: Optional[Any] = None, ++ ignore_errors: bool = False, + ) -> dns.message.Message: + """Return the response obtained after sending a query via UDP. + +@@ -681,6 +700,10 @@ def udp( + if a socket is provided, it must be a nonblocking datagram socket, + and the *source* and *source_port* are ignored. + ++ *ignore_errors*, a ``bool``. If various format errors or response ++ mismatches occur, ignore them and keep listening for a valid response. ++ The default is ``False``. ++ + Returns a ``dns.message.Message``. + """ + +@@ -705,9 +728,13 @@ def udp( + q.mac, + ignore_trailing, + raise_on_truncation, ++ ignore_errors, ++ q, + ) + r.time = received_time - begin_time +- if not q.is_response(r): ++ # We don't need to check q.is_response() if we are in ignore_errors mode ++ # as receive_udp() will have checked it. ++ if not (ignore_errors or q.is_response(r)): + raise BadResponse + return r + assert ( +@@ -727,48 +754,50 @@ def udp_with_fallback( + ignore_trailing: bool = False, + udp_sock: Optional[Any] = None, + tcp_sock: Optional[Any] = None, ++ ignore_errors: bool = False, + ) -> Tuple[dns.message.Message, bool]: + """Return the response to the query, trying UDP first and falling back + to TCP if UDP results in a truncated response. + + *q*, a ``dns.message.Message``, the query to send + +- *where*, a ``str`` containing an IPv4 or IPv6 address, where +- to send the message. ++ *where*, a ``str`` containing an IPv4 or IPv6 address, where to send the message. + +- *timeout*, a ``float`` or ``None``, the number of seconds to wait before the +- query times out. If ``None``, the default, wait forever. ++ *timeout*, a ``float`` or ``None``, the number of seconds to wait before the query ++ times out. If ``None``, the default, wait forever. + + *port*, an ``int``, the port send the message to. The default is 53. + +- *source*, a ``str`` containing an IPv4 or IPv6 address, specifying +- the source address. The default is the wildcard address. ++ *source*, a ``str`` containing an IPv4 or IPv6 address, specifying the source ++ address. The default is the wildcard address. + +- *source_port*, an ``int``, the port from which to send the message. +- The default is 0. ++ *source_port*, an ``int``, the port from which to send the message. The default is ++ 0. + +- *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from +- unexpected sources. ++ *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from unexpected ++ sources. + +- *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own +- RRset. ++ *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own RRset. + +- *ignore_trailing*, a ``bool``. If ``True``, ignore trailing +- junk at end of the received message. ++ *ignore_trailing*, a ``bool``. If ``True``, ignore trailing junk at end of the ++ received message. + +- *udp_sock*, a ``socket.socket``, or ``None``, the socket to use for the +- UDP query. If ``None``, the default, a socket is created. Note that +- if a socket is provided, it must be a nonblocking datagram socket, +- and the *source* and *source_port* are ignored for the UDP query. ++ *udp_sock*, a ``socket.socket``, or ``None``, the socket to use for the UDP query. ++ If ``None``, the default, a socket is created. Note that if a socket is provided, ++ it must be a nonblocking datagram socket, and the *source* and *source_port* are ++ ignored for the UDP query. + + *tcp_sock*, a ``socket.socket``, or ``None``, the connected socket to use for the +- TCP query. If ``None``, the default, a socket is created. Note that +- if a socket is provided, it must be a nonblocking connected stream +- socket, and *where*, *source* and *source_port* are ignored for the TCP +- query. ++ TCP query. If ``None``, the default, a socket is created. Note that if a socket is ++ provided, it must be a nonblocking connected stream socket, and *where*, *source* ++ and *source_port* are ignored for the TCP query. ++ ++ *ignore_errors*, a ``bool``. If various format errors or response mismatches occur ++ while listening for UDP, ignore them and keep listening for a valid response. The ++ default is ``False``. + +- Returns a (``dns.message.Message``, tcp) tuple where tcp is ``True`` +- if and only if TCP was used. ++ Returns a (``dns.message.Message``, tcp) tuple where tcp is ``True`` if and only if ++ TCP was used. + """ + try: + response = udp( +@@ -783,6 +812,7 @@ def udp_with_fallback( + ignore_trailing, + True, + udp_sock, ++ ignore_errors, + ) + return (response, False) + except dns.message.Truncated: +-- +2.39.1 + + diff --git a/0002-For-the-Tudoor-fix-we-also-need-the-UDP-nameserver-t.patch b/0002-For-the-Tudoor-fix-we-also-need-the-UDP-nameserver-t.patch new file mode 100644 index 0000000000000000000000000000000000000000..077b9df384ecf5c61db9c2cc7840567431fc3290 --- /dev/null +++ b/0002-For-the-Tudoor-fix-we-also-need-the-UDP-nameserver-t.patch @@ -0,0 +1,35 @@ +From e093299a49967696b1c58b68e4767de5031a3e46 Mon Sep 17 00:00:00 2001 +From: Bob Halley +Date: Fri, 16 Feb 2024 05:47:35 -0800 +Subject: [PATCH] For the Tudoor fix, we also need the UDP nameserver to + ignore_unexpected. + +(cherry picked from commit 5a441b9854425c4e23abb8f91973361fe8401e33) +--- + dns/nameserver.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/dns/nameserver.py b/dns/nameserver.py +index 030057b..5dbb4e8 100644 +--- a/dns/nameserver.py ++++ b/dns/nameserver.py +@@ -116,6 +116,7 @@ class Do53Nameserver(AddressAndPortNameserver): + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ignore_errors=True, ++ ignore_unexpected=True, + ) + return response + +@@ -155,6 +156,7 @@ class Do53Nameserver(AddressAndPortNameserver): + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ignore_errors=True, ++ ignore_unexpected=True, + ) + return response + +-- +2.39.1 + + diff --git a/0003-test-IgnoreErrors.patch b/0003-test-IgnoreErrors.patch new file mode 100644 index 0000000000000000000000000000000000000000..fed5c7c3178008dd8e133c8c3d02a1574693c17f --- /dev/null +++ b/0003-test-IgnoreErrors.patch @@ -0,0 +1,173 @@ +From ac6763f1018458835201b38cae848e4d261f3e5c Mon Sep 17 00:00:00 2001 +From: Bob Halley +Date: Fri, 16 Feb 2024 07:14:49 -0800 +Subject: [PATCH] test IgnoreErrors + +--- + tests/test_query.py | 140 ++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 140 insertions(+) + +diff --git a/tests/test_query.py b/tests/test_query.py +index 1116b2d..a47daa4 100644 +--- a/tests/test_query.py ++++ b/tests/test_query.py +@@ -15,6 +15,7 @@ + # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + ++import contextlib + import socket + import sys + import time +@@ -32,6 +33,7 @@ import dns.inet + import dns.message + import dns.name + import dns.query ++import dns.rcode + import dns.rdataclass + import dns.rdatatype + import dns.tsigkeyring +@@ -659,3 +661,141 @@ class MiscTests(unittest.TestCase): + dns.query._matches_destination( + socket.AF_INET, ("10.0.0.1", 1234), ("10.0.0.1", 1235), False + ) ++ ++ ++@contextlib.contextmanager ++def mock_udp_recv(wire1, from1, wire2, from2): ++ saved = dns.query._udp_recv ++ first_time = True ++ ++ def mock(sock, max_size, expiration): ++ nonlocal first_time ++ if first_time: ++ first_time = False ++ return wire1, from1 ++ else: ++ return wire2, from2 ++ ++ try: ++ dns.query._udp_recv = mock ++ yield None ++ finally: ++ dns.query._udp_recv = saved ++ ++ ++class IgnoreErrors(unittest.TestCase): ++ def setUp(self): ++ self.q = dns.message.make_query("example.", "A") ++ self.good_r = dns.message.make_response(self.q) ++ self.good_r.set_rcode(dns.rcode.NXDOMAIN) ++ self.good_r_wire = self.good_r.to_wire() ++ ++ def mock_receive( ++ self, ++ wire1, ++ from1, ++ wire2, ++ from2, ++ ignore_unexpected=True, ++ ignore_errors=True, ++ ): ++ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) ++ try: ++ with mock_udp_recv(wire1, from1, wire2, from2): ++ (r, when) = dns.query.receive_udp( ++ s, ++ ("127.0.0.1", 53), ++ time.time() + 2, ++ ignore_unexpected=ignore_unexpected, ++ ignore_errors=ignore_errors, ++ query=self.q, ++ ) ++ self.assertEqual(r, self.good_r) ++ finally: ++ s.close() ++ ++ def test_good_mock(self): ++ self.mock_receive(self.good_r_wire, ("127.0.0.1", 53), None, None) ++ ++ def test_bad_address(self): ++ self.mock_receive( ++ self.good_r_wire, ("127.0.0.2", 53), self.good_r_wire, ("127.0.0.1", 53) ++ ) ++ ++ def test_bad_address_not_ignored(self): ++ def bad(): ++ self.mock_receive( ++ self.good_r_wire, ++ ("127.0.0.2", 53), ++ self.good_r_wire, ++ ("127.0.0.1", 53), ++ ignore_unexpected=False, ++ ) ++ ++ self.assertRaises(dns.query.UnexpectedSource, bad) ++ ++ def test_bad_id(self): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r_wire = bad_r.to_wire() ++ self.mock_receive( ++ bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53) ++ ) ++ ++ def test_bad_id_not_ignored(self): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r_wire = bad_r.to_wire() ++ ++ def bad(): ++ (r, wire) = self.mock_receive( ++ bad_r_wire, ++ ("127.0.0.1", 53), ++ self.good_r_wire, ++ ("127.0.0.1", 53), ++ ignore_errors=False, ++ ) ++ ++ self.assertRaises(AssertionError, bad) ++ ++ def test_bad_wire(self): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r_wire = bad_r.to_wire() ++ self.mock_receive( ++ bad_r_wire[:10], ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53) ++ ) ++ ++ def test_bad_wire_not_ignored(self): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r_wire = bad_r.to_wire() ++ ++ def bad(): ++ self.mock_receive( ++ bad_r_wire[:10], ++ ("127.0.0.1", 53), ++ self.good_r_wire, ++ ("127.0.0.1", 53), ++ ignore_errors=False, ++ ) ++ ++ self.assertRaises(dns.message.ShortHeader, bad) ++ ++ def test_trailing_wire(self): ++ wire = self.good_r_wire + b"abcd" ++ self.mock_receive(wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53)) ++ ++ def test_trailing_wire_not_ignored(self): ++ wire = self.good_r_wire + b"abcd" ++ ++ def bad(): ++ self.mock_receive( ++ wire, ++ ("127.0.0.1", 53), ++ self.good_r_wire, ++ ("127.0.0.1", 53), ++ ignore_errors=False, ++ ) ++ ++ self.assertRaises(dns.message.TrailingJunk, bad) +-- +2.39.1 diff --git a/0004-Further-improve-CVE-fix-coverage-to-100-for-sync-and.patch b/0004-Further-improve-CVE-fix-coverage-to-100-for-sync-and.patch new file mode 100644 index 0000000000000000000000000000000000000000..b9bc622d6ad61792d5c108a772563c8893fc98d7 --- /dev/null +++ b/0004-Further-improve-CVE-fix-coverage-to-100-for-sync-and.patch @@ -0,0 +1,258 @@ +From a1a998938b7370dae41784f8bc0a841dc2addba9 Mon Sep 17 00:00:00 2001 +From: Bob Halley +Date: Fri, 16 Feb 2024 08:46:24 -0800 +Subject: [PATCH] Further improve CVE fix coverage to 100% for sync and async. + +--- + tests/test_async.py | 184 +++++++++++++++++++++++++++++++++++++++++++- + tests/test_query.py | 21 +++++ + 2 files changed, 204 insertions(+), 1 deletion(-) + +diff --git a/tests/test_async.py b/tests/test_async.py +index 4ea2301..ba2078c 100644 +--- a/tests/test_async.py ++++ b/tests/test_async.py +@@ -18,7 +18,6 @@ + import asyncio + import random + import socket +-import sys + import time + import unittest + +@@ -28,6 +27,7 @@ import dns.asyncresolver + import dns.message + import dns.name + import dns.query ++import dns.rcode + import dns.rdataclass + import dns.rdatatype + import dns.resolver +@@ -664,3 +664,185 @@ try: + + except ImportError: + pass ++ ++ ++class MockSock: ++ def __init__(self, wire1, from1, wire2, from2): ++ self.family = socket.AF_INET ++ self.first_time = True ++ self.wire1 = wire1 ++ self.from1 = from1 ++ self.wire2 = wire2 ++ self.from2 = from2 ++ ++ async def sendto(self, data, where, timeout): ++ return len(data) ++ ++ async def recvfrom(self, bufsize, expiration): ++ if self.first_time: ++ self.first_time = False ++ return self.wire1, self.from1 ++ else: ++ return self.wire2, self.from2 ++ ++ ++class IgnoreErrors(unittest.TestCase): ++ def setUp(self): ++ self.q = dns.message.make_query("example.", "A") ++ self.good_r = dns.message.make_response(self.q) ++ self.good_r.set_rcode(dns.rcode.NXDOMAIN) ++ self.good_r_wire = self.good_r.to_wire() ++ dns.asyncbackend.set_default_backend("asyncio") ++ ++ def async_run(self, afunc): ++ return asyncio.run(afunc()) ++ ++ async def mock_receive( ++ self, ++ wire1, ++ from1, ++ wire2, ++ from2, ++ ignore_unexpected=True, ++ ignore_errors=True, ++ ): ++ s = MockSock(wire1, from1, wire2, from2) ++ (r, when, _) = await dns.asyncquery.receive_udp( ++ s, ++ ("127.0.0.1", 53), ++ time.time() + 2, ++ ignore_unexpected=ignore_unexpected, ++ ignore_errors=ignore_errors, ++ query=self.q, ++ ) ++ self.assertEqual(r, self.good_r) ++ ++ def test_good_mock(self): ++ async def run(): ++ await self.mock_receive(self.good_r_wire, ("127.0.0.1", 53), None, None) ++ ++ self.async_run(run) ++ ++ def test_bad_address(self): ++ async def run(): ++ await self.mock_receive( ++ self.good_r_wire, ("127.0.0.2", 53), self.good_r_wire, ("127.0.0.1", 53) ++ ) ++ ++ self.async_run(run) ++ ++ def test_bad_address_not_ignored(self): ++ async def abad(): ++ await self.mock_receive( ++ self.good_r_wire, ++ ("127.0.0.2", 53), ++ self.good_r_wire, ++ ("127.0.0.1", 53), ++ ignore_unexpected=False, ++ ) ++ ++ def bad(): ++ self.async_run(abad) ++ ++ self.assertRaises(dns.query.UnexpectedSource, bad) ++ ++ def test_not_response_not_ignored_udp_level(self): ++ async def abad(): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r_wire = bad_r.to_wire() ++ s = MockSock( ++ bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53) ++ ) ++ await dns.asyncquery.udp(self.good_r, "127.0.0.1", sock=s) ++ ++ def bad(): ++ self.async_run(abad) ++ ++ self.assertRaises(dns.query.BadResponse, bad) ++ ++ def test_bad_id(self): ++ async def run(): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r_wire = bad_r.to_wire() ++ await self.mock_receive( ++ bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53) ++ ) ++ ++ self.async_run(run) ++ ++ def test_bad_id_not_ignored(self): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r_wire = bad_r.to_wire() ++ ++ async def abad(): ++ (r, wire) = await self.mock_receive( ++ bad_r_wire, ++ ("127.0.0.1", 53), ++ self.good_r_wire, ++ ("127.0.0.1", 53), ++ ignore_errors=False, ++ ) ++ ++ def bad(): ++ self.async_run(abad) ++ ++ self.assertRaises(AssertionError, bad) ++ ++ def test_bad_wire(self): ++ async def run(): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r_wire = bad_r.to_wire() ++ await self.mock_receive( ++ bad_r_wire[:10], ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53) ++ ) ++ ++ self.async_run(run) ++ ++ def test_bad_wire_not_ignored(self): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r_wire = bad_r.to_wire() ++ ++ async def abad(): ++ await self.mock_receive( ++ bad_r_wire[:10], ++ ("127.0.0.1", 53), ++ self.good_r_wire, ++ ("127.0.0.1", 53), ++ ignore_errors=False, ++ ) ++ ++ def bad(): ++ self.async_run(abad) ++ ++ self.assertRaises(dns.message.ShortHeader, bad) ++ ++ def test_trailing_wire(self): ++ async def run(): ++ wire = self.good_r_wire + b"abcd" ++ await self.mock_receive( ++ wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53) ++ ) ++ ++ self.async_run(run) ++ ++ def test_trailing_wire_not_ignored(self): ++ wire = self.good_r_wire + b"abcd" ++ ++ async def abad(): ++ await self.mock_receive( ++ wire, ++ ("127.0.0.1", 53), ++ self.good_r_wire, ++ ("127.0.0.1", 53), ++ ignore_errors=False, ++ ) ++ ++ def bad(): ++ self.async_run(abad) ++ ++ self.assertRaises(dns.message.TrailingJunk, bad) +diff --git a/tests/test_query.py b/tests/test_query.py +index a47daa4..1039a14 100644 +--- a/tests/test_query.py ++++ b/tests/test_query.py +@@ -683,6 +683,14 @@ def mock_udp_recv(wire1, from1, wire2, from2): + dns.query._udp_recv = saved + + ++class MockSock: ++ def __init__(self): ++ self.family = socket.AF_INET ++ ++ def sendto(self, data, where): ++ return len(data) ++ ++ + class IgnoreErrors(unittest.TestCase): + def setUp(self): + self.q = dns.message.make_query("example.", "A") +@@ -758,6 +766,19 @@ class IgnoreErrors(unittest.TestCase): + + self.assertRaises(AssertionError, bad) + ++ def test_not_response_not_ignored_udp_level(self): ++ def bad(): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r_wire = bad_r.to_wire() ++ with mock_udp_recv( ++ bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53) ++ ): ++ s = MockSock() ++ dns.query.udp(self.good_r, "127.0.0.1", sock=s) ++ ++ self.assertRaises(dns.query.BadResponse, bad) ++ + def test_bad_wire(self): + bad_r = dns.message.make_response(self.q) + bad_r.id += 1 +-- +2.39.1 + diff --git a/0005-Ensure-asyncio-datagram-sockets-on-windows-have-had-.patch b/0005-Ensure-asyncio-datagram-sockets-on-windows-have-had-.patch new file mode 100644 index 0000000000000000000000000000000000000000..76bff334ee792349ef0f70f63369dd1da72bde39 --- /dev/null +++ b/0005-Ensure-asyncio-datagram-sockets-on-windows-have-had-.patch @@ -0,0 +1,141 @@ +From adfc942725bd36d28ec53f7e5480ace9eb543bd8 Mon Sep 17 00:00:00 2001 +From: Bob Halley +Date: Thu, 14 Dec 2023 18:04:39 -0800 +Subject: [PATCH] Ensure asyncio datagram sockets on windows have had a bind() + before recvfrom(). + +The fix for [#637] erroneously concluded that that windows asyncio +needed connected datagram sockets, but subsequent further +investation showed that the actual problem was that windows wants +an unconnected datagram socket to be bound before recvfrom is called. +Linux autobinds in this case to the wildcard address and port, so +that's why we didn't see any problems there. We now ensure that +the source is bound. +--- + dns/_asyncio_backend.py | 13 ++++++------- + tests/test_async.py | 25 +++++-------------------- + 2 files changed, 11 insertions(+), 27 deletions(-) + +diff --git a/dns/_asyncio_backend.py b/dns/_asyncio_backend.py +index 2631228..7d4d1b5 100644 +--- a/dns/_asyncio_backend.py ++++ b/dns/_asyncio_backend.py +@@ -8,6 +8,7 @@ import sys + + import dns._asyncbackend + import dns.exception ++import dns.inet + + _is_win32 = sys.platform == "win32" + +@@ -224,14 +225,12 @@ class Backend(dns._asyncbackend.Backend): + ssl_context=None, + server_hostname=None, + ): +- if destination is None and socktype == socket.SOCK_DGRAM and _is_win32: +- raise NotImplementedError( +- "destinationless datagram sockets " +- "are not supported by asyncio " +- "on Windows" +- ) + loop = _get_running_loop() + if socktype == socket.SOCK_DGRAM: ++ if _is_win32 and source is None: ++ # Win32 wants explicit binding before recvfrom(). This is the ++ # proper fix for [#637]. ++ source = (dns.inet.any_for_af(af), 0) + transport, protocol = await loop.create_datagram_endpoint( + _DatagramProtocol, + source, +@@ -266,7 +265,7 @@ class Backend(dns._asyncbackend.Backend): + await asyncio.sleep(interval) + + def datagram_connection_required(self): +- return _is_win32 ++ return False + + def get_transport_class(self): + return _HTTPTransport +diff --git a/tests/test_async.py b/tests/test_async.py +index d0f977a..ac32431 100644 +--- a/tests/test_async.py ++++ b/tests/test_async.py +@@ -171,8 +171,6 @@ class MiscQuery(unittest.TestCase): + + @unittest.skipIf(not tests.util.is_internet_reachable(), "Internet not reachable") + class AsyncTests(unittest.TestCase): +- connect_udp = sys.platform == "win32" +- + def setUp(self): + self.backend = dns.asyncbackend.set_default_backend("asyncio") + +@@ -327,12 +325,12 @@ class AsyncTests(unittest.TestCase): + qname = dns.name.from_text("dns.google.") + + async def run(): +- if self.connect_udp: +- dtuple = (address, 53) +- else: +- dtuple = None + async with await self.backend.make_socket( +- dns.inet.af_for_address(address), socket.SOCK_DGRAM, 0, None, dtuple ++ dns.inet.af_for_address(address), ++ socket.SOCK_DGRAM, ++ 0, ++ None, ++ None, + ) as s: + q = dns.message.make_query(qname, dns.rdatatype.A) + return await dns.asyncquery.udp(q, address, sock=s, timeout=2) +@@ -485,9 +483,6 @@ class AsyncTests(unittest.TestCase): + self.assertFalse(tcp) + + def testUDPReceiveQuery(self): +- if self.connect_udp: +- self.skipTest("test needs connectionless sockets") +- + async def run(): + async with await self.backend.make_socket( + socket.AF_INET, socket.SOCK_DGRAM, source=("127.0.0.1", 0) +@@ -509,9 +504,6 @@ class AsyncTests(unittest.TestCase): + self.assertEqual(sender_address, recv_address) + + def testUDPReceiveTimeout(self): +- if self.connect_udp: +- self.skipTest("test needs connectionless sockets") +- + async def arun(): + async with await self.backend.make_socket( + socket.AF_INET, socket.SOCK_DGRAM, 0, ("127.0.0.1", 0) +@@ -616,8 +608,6 @@ class AsyncTests(unittest.TestCase): + + @unittest.skipIf(not tests.util.is_internet_reachable(), "Internet not reachable") + class AsyncioOnlyTests(unittest.TestCase): +- connect_udp = sys.platform == "win32" +- + def setUp(self): + self.backend = dns.asyncbackend.set_default_backend("asyncio") + +@@ -625,9 +615,6 @@ class AsyncioOnlyTests(unittest.TestCase): + return asyncio.run(afunc()) + + def testUseAfterTimeout(self): +- if self.connect_udp: +- self.skipTest("test needs connectionless sockets") +- + # Test #843 fix. + async def run(): + qname = dns.name.from_text("dns.google") +@@ -678,8 +665,6 @@ try: + return trio.run(afunc) + + class TrioAsyncTests(AsyncTests): +- connect_udp = False +- + def setUp(self): + self.backend = dns.asyncbackend.set_default_backend("trio") + +-- +2.39.1 + + diff --git a/0006-The-Tudoor-fix-should-not-eat-valid-Truncated-except.patch b/0006-The-Tudoor-fix-should-not-eat-valid-Truncated-except.patch new file mode 100644 index 0000000000000000000000000000000000000000..bc145569e1f1caeb5fe27247758634091bbf5265 --- /dev/null +++ b/0006-The-Tudoor-fix-should-not-eat-valid-Truncated-except.patch @@ -0,0 +1,229 @@ +From 2ab3d1628c9ae0545e225522b3b445c3478dc6ad Mon Sep 17 00:00:00 2001 +From: Bob Halley +Date: Sun, 18 Feb 2024 10:27:43 -0800 +Subject: [PATCH] The Tudoor fix should not eat valid Truncated exceptions + [#1053] (#1054) + +* The Tudoor fix should not eat valid Truncated exceptions [##1053] + +* Make logic more readable +--- + dns/asyncquery.py | 10 ++++++++ + dns/query.py | 14 +++++++++++ + tests/test_async.py | 60 ++++++++++++++++++++++++++++++++++++++++++++- + tests/test_query.py | 44 ++++++++++++++++++++++++++++++++- + 4 files changed, 126 insertions(+), 2 deletions(-) + +diff --git a/dns/asyncquery.py b/dns/asyncquery.py +index 94cb2413..4d9ab9ae 100644 +--- a/dns/asyncquery.py ++++ b/dns/asyncquery.py +@@ -151,6 +151,16 @@ async def receive_udp( + ignore_trailing=ignore_trailing, + raise_on_truncation=raise_on_truncation, + ) ++ except dns.message.Truncated as e: ++ # See the comment in query.py for details. ++ if ( ++ ignore_errors ++ and query is not None ++ and not query.is_response(e.message()) ++ ): ++ continue ++ else: ++ raise + except Exception: + if ignore_errors: + continue +diff --git a/dns/query.py b/dns/query.py +index 06d186c7..384bf31e 100644 +--- a/dns/query.py ++++ b/dns/query.py +@@ -618,6 +618,20 @@ def receive_udp( + ignore_trailing=ignore_trailing, + raise_on_truncation=raise_on_truncation, + ) ++ except dns.message.Truncated as e: ++ # If we got Truncated and not FORMERR, we at least got the header with TC ++ # set, and very likely the question section, so we'll re-raise if the ++ # message seems to be a response as we need to know when truncation happens. ++ # We need to check that it seems to be a response as we don't want a random ++ # injected message with TC set to cause us to bail out. ++ if ( ++ ignore_errors ++ and query is not None ++ and not query.is_response(e.message()) ++ ): ++ continue ++ else: ++ raise + except Exception: + if ignore_errors: + continue +diff --git a/tests/test_async.py b/tests/test_async.py +index ba2078cd..9373548d 100644 +--- a/tests/test_async.py ++++ b/tests/test_async.py +@@ -705,7 +705,11 @@ async def mock_receive( + from2, + ignore_unexpected=True, + ignore_errors=True, ++ raise_on_truncation=False, ++ good_r=None, + ): ++ if good_r is None: ++ good_r = self.good_r + s = MockSock(wire1, from1, wire2, from2) + (r, when, _) = await dns.asyncquery.receive_udp( + s, +@@ -713,9 +717,10 @@ async def mock_receive( + time.time() + 2, + ignore_unexpected=ignore_unexpected, + ignore_errors=ignore_errors, ++ raise_on_truncation=raise_on_truncation, + query=self.q, + ) +- self.assertEqual(r, self.good_r) ++ self.assertEqual(r, good_r) + + def test_good_mock(self): + async def run(): +@@ -802,6 +807,59 @@ async def run(): + + self.async_run(run) + ++ def test_good_wire_with_truncation_flag_and_no_truncation_raise(self): ++ async def run(): ++ tc_r = dns.message.make_response(self.q) ++ tc_r.flags |= dns.flags.TC ++ tc_r_wire = tc_r.to_wire() ++ await self.mock_receive( ++ tc_r_wire, ("127.0.0.1", 53), None, None, good_r=tc_r ++ ) ++ ++ self.async_run(run) ++ ++ def test_good_wire_with_truncation_flag_and_truncation_raise(self): ++ async def agood(): ++ tc_r = dns.message.make_response(self.q) ++ tc_r.flags |= dns.flags.TC ++ tc_r_wire = tc_r.to_wire() ++ await self.mock_receive( ++ tc_r_wire, ("127.0.0.1", 53), None, None, raise_on_truncation=True ++ ) ++ ++ def good(): ++ self.async_run(agood) ++ ++ self.assertRaises(dns.message.Truncated, good) ++ ++ def test_wrong_id_wire_with_truncation_flag_and_no_truncation_raise(self): ++ async def run(): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r.flags |= dns.flags.TC ++ bad_r_wire = bad_r.to_wire() ++ await self.mock_receive( ++ bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53) ++ ) ++ ++ self.async_run(run) ++ ++ def test_wrong_id_wire_with_truncation_flag_and_truncation_raise(self): ++ async def run(): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r.flags |= dns.flags.TC ++ bad_r_wire = bad_r.to_wire() ++ await self.mock_receive( ++ bad_r_wire, ++ ("127.0.0.1", 53), ++ self.good_r_wire, ++ ("127.0.0.1", 53), ++ raise_on_truncation=True, ++ ) ++ ++ self.async_run(run) ++ + def test_bad_wire_not_ignored(self): + bad_r = dns.message.make_response(self.q) + bad_r.id += 1 +diff --git a/tests/test_query.py b/tests/test_query.py +index 1039a14e..62007e85 100644 +--- a/tests/test_query.py ++++ b/tests/test_query.py +@@ -29,6 +29,7 @@ + have_ssl = False + + import dns.exception ++import dns.flags + import dns.inet + import dns.message + import dns.name +@@ -706,7 +707,11 @@ def mock_receive( + from2, + ignore_unexpected=True, + ignore_errors=True, ++ raise_on_truncation=False, ++ good_r=None, + ): ++ if good_r is None: ++ good_r = self.good_r + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + with mock_udp_recv(wire1, from1, wire2, from2): +@@ -716,9 +721,10 @@ def mock_receive( + time.time() + 2, + ignore_unexpected=ignore_unexpected, + ignore_errors=ignore_errors, ++ raise_on_truncation=raise_on_truncation, + query=self.q, + ) +- self.assertEqual(r, self.good_r) ++ self.assertEqual(r, good_r) + finally: + s.close() + +@@ -787,6 +793,42 @@ def test_bad_wire(self): + bad_r_wire[:10], ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53) + ) + ++ def test_good_wire_with_truncation_flag_and_no_truncation_raise(self): ++ tc_r = dns.message.make_response(self.q) ++ tc_r.flags |= dns.flags.TC ++ tc_r_wire = tc_r.to_wire() ++ self.mock_receive(tc_r_wire, ("127.0.0.1", 53), None, None, good_r=tc_r) ++ ++ def test_good_wire_with_truncation_flag_and_truncation_raise(self): ++ def good(): ++ tc_r = dns.message.make_response(self.q) ++ tc_r.flags |= dns.flags.TC ++ tc_r_wire = tc_r.to_wire() ++ self.mock_receive( ++ tc_r_wire, ("127.0.0.1", 53), None, None, raise_on_truncation=True ++ ) ++ ++ self.assertRaises(dns.message.Truncated, good) ++ ++ def test_wrong_id_wire_with_truncation_flag_and_no_truncation_raise(self): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r.flags |= dns.flags.TC ++ bad_r_wire = bad_r.to_wire() ++ self.mock_receive( ++ bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53) ++ ) ++ ++ def test_wrong_id_wire_with_truncation_flag_and_truncation_raise(self): ++ bad_r = dns.message.make_response(self.q) ++ bad_r.id += 1 ++ bad_r.flags |= dns.flags.TC ++ bad_r_wire = bad_r.to_wire() ++ self.mock_receive( ++ bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53), ++ raise_on_truncation=True ++ ) ++ + def test_bad_wire_not_ignored(self): + bad_r = dns.message.make_response(self.q) + bad_r.id += 1 diff --git a/python-dns.spec b/python-dns.spec index a5c76566c7650c92e266841fc975b0f65efa9e2c..4b99be8237bd656732301c3034304682b266302f 100644 --- a/python-dns.spec +++ b/python-dns.spec @@ -14,11 +14,19 @@ messages, names, and records. Name: python-dns Summary: %{sum} Version: 2.4.2 -Release: 1 +Release: 2 License: ISC and MIT URL: http://www.dnspython.org/ Source0: https://github.com/rthalley/dnspython/archive/v%{version}/dnspython-%{version}.tar.gz +# fix CVE-2023-29483 +Patch0001: 0001-Address-DoS-via-the-Tudoor-mechanism-CVE-2023-29483-.patch +Patch0002: 0002-For-the-Tudoor-fix-we-also-need-the-UDP-nameserver-t.patch +Patch0003: 0003-test-IgnoreErrors.patch +Patch0004: 0004-Further-improve-CVE-fix-coverage-to-100-for-sync-and.patch +Patch0005: 0005-Ensure-asyncio-datagram-sockets-on-windows-have-had-.patch +Patch0006: 0006-The-Tudoor-fix-should-not-eat-valid-Truncated-except.patch + BuildArch: noarch BuildRequires: python3-devel python3-setuptools python3-cryptography @@ -60,6 +68,9 @@ pytest %doc examples %changelog +* Thu Apr 18 2024 wangguochun - 2.4.2-2 +- fix CVE-2023-29483 + * Tue Dec 26 2023 gaihuiying - 2.4.2-1 - update to 2.4.2