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