File CVE-2023-29483.patch of Package python-dnspython.34934

From 093c593624bcf55766c2a952c207e0b92920214e Mon Sep 17 00:00:00 2001
From: Bob Halley <halley@dnspython.org>
Date: Fri, 9 Feb 2024 10:36:08 -0800
Subject: [PATCH] Address DoS via the Tudoor mechanism (CVE-2023-29483)

---
 dns/query.py      | 110 +++++++++++++++++++++++++++++-----------------
 3 files changed, 103 insertions(+), 54 deletions(-)

Index: dnspython-1.15.0/dns/query.py
===================================================================
--- dnspython-1.15.0.orig/dns/query.py
+++ dnspython-1.15.0/dns/query.py
@@ -58,6 +58,34 @@ def _compute_expiration(timeout):
         return time.time() + timeout
 
 
+def _matches_destination(af, from_address, destination, ignore_unexpected):
+    # Check that from_address is appropriate for a response to a query
+    # sent to destination.
+    if not destination:
+        return True
+    if _addresses_equal(af, from_address, destination) or (
+        dns.inet.is_multicast(destination[0]) and from_address[1:] == destination[1:]
+    ):
+        return True
+    elif ignore_unexpected:
+        return False
+    raise UnexpectedSource('got a response from '
+                           '%s instead of %s' % (from_address,
+                                                 destination))
+
+
+def _udp_recv(sock, max_size, expiration):
+    """Reads a datagram from the socket.
+    A Timeout exception will be raised if the operation is not completed
+    by the expiration time.
+    """
+    while True:
+        try:
+            return sock.recvfrom(max_size)
+        except BlockingIOError:
+            _wait_for_readable(sock, expiration)
+
+
 def _poll_for(fd, readable, writable, error, timeout):
     """Poll polling backend.
     @param fd: File descriptor
@@ -120,6 +148,9 @@ def _select_for(fd, readable, writable,
 
 def _wait_for(fd, readable, writable, error, expiration):
     done = False
+    if hasattr(fd, 'test_mock'):
+        return
+
     while not done:
         if expiration is None:
             timeout = None
@@ -217,7 +248,8 @@ def send_udp(sock, what, destination, ex
 
 def receive_udp(sock, destination, expiration=None, af=None,
                 ignore_unexpected=False, one_rr_per_rrset=False,
-                keyring=None, request_mac=b''):
+                keyring=None, request_mac=b'', ignore_errors=False,
+                query=None):
     """Read a DNS message from a UDP socket.
 
     @param sock: the socket
@@ -235,6 +267,14 @@ def receive_udp(sock, destination, expir
     @type keyring: keyring dict
     @param request_mac: the MAC of the request (for TSIG)
     @type request_mac: bytes
+    @param ignore_errors: If various format errors or response
+    mismatches occur, ignore them and keep listening for a valid response.
+    The default is ``False``.
+    @type ignore_errors: bool
+    @param query: a 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.
+    @type query: dns.message.Message object
     @rtype: dns.message.Message object
     """
     if af is None:
@@ -244,23 +284,43 @@ def receive_udp(sock, destination, expir
             af = dns.inet.AF_INET
     wire = b''
     while 1:
-        _wait_for_readable(sock, expiration)
-        (wire, from_address) = sock.recvfrom(65535)
-        if _addresses_equal(af, from_address, destination) or \
-           (dns.inet.is_multicast(destination[0]) and
-            from_address[1:] == destination[1:]):
-            break
-        if not ignore_unexpected:
-            raise UnexpectedSource('got a response from '
-                                   '%s instead of %s' % (from_address,
-                                                         destination))
-    received_time = time.time()
-    r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac,
-                              one_rr_per_rrset=one_rr_per_rrset)
-    return (r, received_time)
+        (wire, from_address) = _udp_recv(sock, 65535, expiration)
+        if not _matches_destination(
+            sock.family, from_address, destination, ignore_unexpected
+        ):
+            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)
+        # 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
+            else:
+                raise
+        if ignore_errors and query is not None and not query.is_response(r):
+            continue
+
+        return (r, received_time)
 
 def udp(q, where, timeout=None, port=53, af=None, source=None, source_port=0,
-        ignore_unexpected=False, one_rr_per_rrset=False):
+        ignore_unexpected=False, one_rr_per_rrset=False,
+        ignore_errors=False, sock=None):
     """Return the response obtained after sending a query via UDP.
 
     @param q: the query
@@ -286,13 +346,20 @@ def udp(q, where, timeout=None, port=53,
     @type ignore_unexpected: bool
     @param one_rr_per_rrset: Put each RR into its own RRset
     @type one_rr_per_rrset: bool
+    @param ignore_errors: If various format errors or response
+    mismatches occur, ignore them and keep listening for a valid response.
+    The default is ``False``.
+    @type ignore_errors: bool
     @rtype: dns.message.Message object
     """
 
     wire = q.to_wire()
     (af, destination, source) = _destination_and_source(af, where, port,
                                                         source, source_port)
-    s = socket_factory(af, socket.SOCK_DGRAM, 0)
+    if sock:
+        s = sock
+    else:
+        s = socket_factory(af, socket.SOCK_DGRAM, 0)
     received_time = None
     sent_time = None
     try:
@@ -303,7 +370,8 @@ def udp(q, where, timeout=None, port=53,
         (_, sent_time) = send_udp(s, wire, destination, expiration)
         (r, received_time) = receive_udp(s, destination, expiration, af,
                                          ignore_unexpected, one_rr_per_rrset,
-                                         q.keyring, q.request_mac)
+                                         q.keyring, q.request_mac,
+                                         ignore_errors, q)
     finally:
         if sent_time is None or received_time is None:
             response_time = 0
@@ -311,7 +379,9 @@ def udp(q, where, timeout=None, port=53,
             response_time = received_time - sent_time
         s.close()
     r.time = response_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
 
@@ -561,8 +631,7 @@ def xfr(where, zone, rdtype=dns.rdatatyp
         if mexpiration is None or mexpiration > expiration:
             mexpiration = expiration
         if use_udp:
-            _wait_for_readable(s, expiration)
-            (wire, from_address) = s.recvfrom(65535)
+            (wire, from_address) = _udp_recv(s, 65535, mexpiration)
         else:
             ldata = _net_read(s, 2, mexpiration)
             (l,) = struct.unpack("!H", ldata)
Index: dnspython-1.15.0/dns/resolver.py
===================================================================
--- dnspython-1.15.0.orig/dns/resolver.py
+++ dnspython-1.15.0/dns/resolver.py
@@ -959,7 +959,9 @@ class Resolver(object):
                             response = dns.query.udp(request, nameserver,
                                                      timeout, port,
                                                      source=source,
-                                                     source_port=source_port)
+                                                     source_port=source_port,
+                                                     ignore_errors=True,
+                                                     ignore_unexpected=True)
                             if response.flags & dns.flags.TC:
                                 # Response truncated; retry with TCP.
                                 tcp_attempt = True
Index: dnspython-1.15.0/tests/test_query.py
===================================================================
--- /dev/null
+++ dnspython-1.15.0/tests/test_query.py
@@ -0,0 +1,195 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import time
+import contextlib
+import unittest
+import socket
+
+import dns.message
+import dns.query
+import dns.rcode
+import dns.zone
+
+
+@contextlib.contextmanager
+def mock_udp_recv(wire1, from1, wire2, from2):
+    saved = dns.query._udp_recv
+    context = {'first_time': True}
+
+    def mock(sock, max_size, expiration):
+        if context['first_time']:
+            context['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 MockSock:
+    def __init__(self):
+        self.family = socket.AF_INET
+        self.test_mock = True
+
+    def sendto(self, data, where):
+        return len(data)
+
+    def close(self):
+        pass
+
+    def setblocking(self, *args, **kwargs):
+        pass
+
+
+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_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
+        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)
+
+
+if __name__ == '__main__':
+    unittest.main()
Index: dnspython-1.15.0/dns/inet.py
===================================================================
--- dnspython-1.15.0.orig/dns/inet.py
+++ dnspython-1.15.0/dns/inet.py
@@ -101,11 +101,11 @@ def is_multicast(text):
     @rtype: bool
     """
     try:
-        first = ord(dns.ipv4.inet_aton(text)[0])
+        first = dns.ipv4.inet_aton(text)[0]
         return first >= 224 and first <= 239
     except Exception:
         try:
-            first = ord(dns.ipv6.inet_aton(text)[0])
+            first = dns.ipv6.inet_aton(text)[0]
             return first == 255
         except Exception:
             raise ValueError
openSUSE Build Service is sponsored by