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) Conflict: change filename nameserver.py to asyncresolver.py/resolver.py and change the context because of refactoring Reference:https://github.com/rthalley/dnspython/commit/f66e25b5f549acf66d1fb6ead13eb3cff7d09af3 --- dns/asyncquery.py | 51 +++++++++++++------ dns/asyncresolver.py | 3 +- dns/query.py | 116 ++++++++++++++++++++++++++++--------------- dns/resolver.py | 3 +- 4 files changed, 115 insertions(+), 58 deletions(-) diff --git a/dns/asyncquery.py b/dns/asyncquery.py index 35a355bb..94cb2413 100644 --- a/dns/asyncquery.py +++ b/dns/asyncquery.py @@ -120,7 +120,9 @@ async def receive_udp( async def receive_udp(sock, destination=None, expiration=None, ignore_unexpected=False, one_rr_per_rrset=False, keyring=None, request_mac=b'', ignore_trailing=False, - raise_on_truncation=False): + raise_on_truncation=False, + ignore_errors=False, + query=None): """Read a DNS message from a UDP socket. *sock*, a ``dns.asyncbackend.DatagramSocket``. @@ -133,17 +135,29 @@ async def receive_udp( """ wire = b'' - while 1: + while True: (wire, from_address) = await sock.recvfrom(65535, _timeout(expiration)) - if _matches_destination(sock.family, from_address, 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(q, where, timeout=None, port=53, source=None, source_port=0, ignore_unexpected=False, one_rr_per_rrset=False, @@ -164,7 +174,8 @@ async def udp( async def udp(q, where, timeout=None, port=53, source=None, source_port=0, ignore_unexpected=False, one_rr_per_rrset=False, ignore_trailing=False, raise_on_truncation=False, sock=None, - backend=None): + backend=None, + ignore_errors=False): """Return the response obtained after sending a query via UDP. *sock*, a ``dns.asyncbackend.DatagramSocket``, or ``None``, @@ -205,9 +216,13 @@ async def udp( one_rr_per_rrset, q.keyring, q.mac, ignore_trailing, - raise_on_truncation) + 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 finally: @@ -225,7 +240,8 @@ async def udp_with_fallback( async def udp_with_fallback(q, where, timeout=None, port=53, source=None, source_port=0, ignore_unexpected=False, one_rr_per_rrset=False, ignore_trailing=False, - udp_sock=None, tcp_sock=None, backend=None): + udp_sock=None, tcp_sock=None, backend=None, + ignore_errors=False): """Return the response to the query, trying UDP first and falling back to TCP if UDP results in a truncated response. @@ -260,7 +276,8 @@ async def udp_with_fallback( try: response = await udp(q, where, timeout, port, source, source_port, ignore_unexpected, one_rr_per_rrset, - ignore_trailing, True, udp_sock, backend) + ignore_trailing, True, udp_sock, backend, + ignore_errors) return (response, False) except dns.message.Truncated: response = await tcp(q, where, timeout, port, source, source_port, diff --git a/dns/resolver.py b/dns/resolver.py index 7da7a61..d3769a0 100644 --- a/dns/resolver.py +++ b/dns/resolver.py @@ -1080,7 +1080,8 @@ class Resolver(BaseResolver): port=port, source=source, source_port=source_port, - raise_on_truncation=True) + raise_on_truncation=True, + ignore_errors=True) else: response = dns.query.https(request, nameserver, timeout=timeout) diff --git a/dns/asyncresolver.py b/dns/asyncresolver.py index ed29dee..5c8fa8a 100644 --- a/dns/asyncresolver.py +++ b/dns/asyncresolver.py @@ -85,7 +85,8 @@ class Resolver(dns.resolver.BaseResolver): timeout, port, source, source_port, raise_on_truncation=True, - backend=backend) + backend=backend, + ignore_errors=True) else: response = await dns.asyncquery.https(request, nameserver, diff --git a/dns/query.py b/dns/query.py index d4bd6b92..bdd251e7 100644 --- a/dns/query.py +++ b/dns/query.py @@ -569,7 +569,9 @@ def receive_udp( def receive_udp(sock, destination=None, expiration=None, ignore_unexpected=False, one_rr_per_rrset=False, keyring=None, request_mac=b'', ignore_trailing=False, - raise_on_truncation=False): + raise_on_truncation=False, + ignore_errors=False, + query=None): """Read a DNS message from a UDP socket. *sock*, a ``socket``. @@ -609,23 +611,43 @@ 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(sock.family, from_address, 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(q, where, timeout=None, port=53, source=None, source_port=0, ignore_unexpected=False, one_rr_per_rrset=False, ignore_trailing=False, @@ -645,7 +663,8 @@ def udp( def udp(q, where, timeout=None, port=53, source=None, source_port=0, ignore_unexpected=False, one_rr_per_rrset=False, ignore_trailing=False, - raise_on_truncation=False, sock=None): + raise_on_truncation=False, sock=None, + ignore_errors=False): """Return the response obtained after sending a query via UDP. *q*, a ``dns.message.Message``, the query to send @@ -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( (r, received_time) = receive_udp(s, destination, expiration, ignore_unexpected, one_rr_per_rrset, q.keyring, q.mac, ignore_trailing, - raise_on_truncation) + 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 @@ -727,48 +754,50 @@ def udp_with_fallback( def udp_with_fallback(q, where, timeout=None, port=53, source=None, source_port=0, ignore_unexpected=False, one_rr_per_rrset=False, ignore_trailing=False, - udp_sock=None, tcp_sock=None): + udp_sock=None, tcp_sock=None, + ignore_errors=False): """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(q, where, timeout, port, source, source_port, @@ -783,7 +812,8 @@ def udp_with_fallback( try: response = udp(q, where, timeout, port, source, source_port, ignore_unexpected, one_rr_per_rrset, - ignore_trailing, True, udp_sock) + ignore_trailing, True, udp_sock, + ignore_errors) return (response, False) except dns.message.Truncated: response = tcp(q, where, timeout, port, source, source_port,