Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Step:15-SP4
python-dnspython.35626
CVE-2023-29483.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File CVE-2023-29483.patch of Package python-dnspython.35626
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.mac) + q.keyring, q.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 @@ -562,8 +632,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
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor