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