diff --git a/backport-001-CVE-2023-29483.patch b/backport-001-CVE-2023-29483.patch new file mode 100644 index 0000000000000000000000000000000000000000..a9b4809d557712dd520547dbbfe0425b6a4a374e --- /dev/null +++ b/backport-001-CVE-2023-29483.patch @@ -0,0 +1,335 @@ +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, diff --git a/backport-002-CVE-2023-29483.patch b/backport-002-CVE-2023-29483.patch new file mode 100644 index 0000000000000000000000000000000000000000..dd88ea0184a8edb56e8409a40e1d624ff17b91dd --- /dev/null +++ b/backport-002-CVE-2023-29483.patch @@ -0,0 +1,44 @@ +From 5a441b9854425c4e23abb8f91973361fe8401e33 Mon Sep 17 00:00:00 2001 +From: Bob Halley +Date: Fri, 16 Feb 2024 05:47:35 -0800 +Subject: [PATCH] For the Tudoor fix, we also need the UDP nameserver to + ignore_unexpected. + +Conflict: change filename and function because of refactoring +Reference:https://github.com/rthalley/dnspython/commit/5a441b9854425c4e23abb8f91973361fe8401e33 +--- + dns/asyncresolver.py | 3 ++- + dns/resolver.py | 3 ++- + 2 files changed, 4 insertions(+), 2 deletions(-) + +diff --git a/dns/asyncresolver.py b/dns/asyncresolver.py +index 5c8fa8a..1e66e08 100644 +--- a/dns/asyncresolver.py ++++ b/dns/asyncresolver.py +@@ -86,7 +86,8 @@ class Resolver(dns.resolver.BaseResolver): + source, source_port, + raise_on_truncation=True, + backend=backend, +- ignore_errors=True) ++ ignore_errors=True, ++ ignore_unexpected=True) + else: + response = await dns.asyncquery.https(request, + nameserver, +diff --git a/dns/resolver.py b/dns/resolver.py +index d3769a0..9c50361 100644 +--- a/dns/resolver.py ++++ b/dns/resolver.py +@@ -1081,7 +1081,8 @@ class Resolver(BaseResolver): + source=source, + source_port=source_port, + raise_on_truncation=True, +- ignore_errors=True) ++ ignore_errors=True, ++ ignore_unexpected=True) + else: + response = dns.query.https(request, nameserver, + timeout=timeout) +-- +2.33.0 + diff --git a/backport-003-CVE-2023-29483.patch b/backport-003-CVE-2023-29483.patch new file mode 100644 index 0000000000000000000000000000000000000000..826a58da7d076d2f3f375dcf02a5fe3a461bc83b --- /dev/null +++ b/backport-003-CVE-2023-29483.patch @@ -0,0 +1,63 @@ +From 2ab3d1628c9ae0545e225522b3b445c3478dc6ad Mon Sep 17 00:00:00 2001 +From: Bob Halley +Date: Sun, 18 Feb 2024 10:27:43 -0800 +Subject: [PATCH] The Tudoor fix should not eat valid Truncated exceptions + [#1053] (#1054) + +* The Tudoor fix should not eat valid Truncated exceptions [##1053] + +* Make logic more readable + +Conflict: delete tests, because no function about mock +Reference:https://github.com/rthalley/dnspython/commit/2ab3d1628c9ae0545e225522b3b445c3478dc6ad +--- + dns/asyncquery.py | 10 ++++++++ + dns/query.py | 14 +++++++++++ + 2 files changed, 24 insertions(+), 0 deletions(-) + +diff --git a/dns/asyncquery.py b/dns/asyncquery.py +index 94cb2413..4d9ab9ae 100644 +--- a/dns/asyncquery.py ++++ b/dns/asyncquery.py +@@ -151,6 +151,16 @@ async def receive_udp( + ignore_trailing=ignore_trailing, + raise_on_truncation=raise_on_truncation, + ) ++ except dns.message.Truncated as e: ++ # See the comment in query.py for details. ++ if ( ++ ignore_errors ++ and query is not None ++ and not query.is_response(e.message()) ++ ): ++ continue ++ else: ++ raise + except Exception: + if ignore_errors: + continue +diff --git a/dns/query.py b/dns/query.py +index 06d186c7..384bf31e 100644 +--- a/dns/query.py ++++ b/dns/query.py +@@ -618,6 +618,20 @@ def receive_udp( + ignore_trailing=ignore_trailing, + raise_on_truncation=raise_on_truncation, + ) ++ 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 diff --git a/python-dns.spec b/python-dns.spec index 818566bbc44a7c855856164a1c648ddd387cee0c..396807ff302680907011b522d3b80bb297fed603 100644 --- a/python-dns.spec +++ b/python-dns.spec @@ -14,7 +14,7 @@ messages, names, and records. Name: python-dns Summary: %{sum} Version: 2.2.1 -Release: 2 +Release: 3 License: ISC and MIT URL: http://www.dnspython.org/ Source0: https://github.com/rthalley/dnspython/archive/v%{version}/dnspython-%{version}.tar.gz @@ -22,6 +22,9 @@ Source0: https://github.com/rthalley/dnspython/archive/v%{version}/dnspyt Patch0: fix-failed-tests.patch Patch1: backport-fix-missing-version-in-setup.cfg.patch Patch2: 0001-update-README-w-codecov-and-lgtm.patch +Patch3: backport-001-CVE-2023-29483.patch +Patch4: backport-002-CVE-2023-29483.patch +Patch5: backport-003-CVE-2023-29483.patch BuildArch: noarch @@ -64,6 +67,9 @@ pytest %doc examples %changelog +* Wed Jul 24 2024 gaihuiying - 2.2.1-3 +- fix CVE-2023-29483 + * Fri Jan 13 2023 caofei - 2.2.1-2 - update README w/ codecov and lgtm