fix CVE-2023-29483

This commit is contained in:
wangguochun 2024-04-24 17:01:11 +08:00
parent cfbddc651b
commit b2d170c54a
7 changed files with 1169 additions and 1 deletions

View File

@ -0,0 +1,321 @@
From f66e25b5f549acf66d1fb6ead13eb3cff7d09af3 Mon Sep 17 00:00:00 2001
From: Bob Halley <halley@dnspython.org>
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

View File

@ -0,0 +1,35 @@
From e093299a49967696b1c58b68e4767de5031a3e46 Mon Sep 17 00:00:00 2001
From: Bob Halley <halley@dnspython.org>
Date: Fri, 16 Feb 2024 05:47:35 -0800
Subject: [PATCH] For the Tudoor fix, we also need the UDP nameserver to
ignore_unexpected.
(cherry picked from commit 5a441b9854425c4e23abb8f91973361fe8401e33)
---
dns/nameserver.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/dns/nameserver.py b/dns/nameserver.py
index 030057b..5dbb4e8 100644
--- a/dns/nameserver.py
+++ b/dns/nameserver.py
@@ -116,6 +116,7 @@ class Do53Nameserver(AddressAndPortNameserver):
one_rr_per_rrset=one_rr_per_rrset,
ignore_trailing=ignore_trailing,
ignore_errors=True,
+ ignore_unexpected=True,
)
return response
@@ -155,6 +156,7 @@ class Do53Nameserver(AddressAndPortNameserver):
one_rr_per_rrset=one_rr_per_rrset,
ignore_trailing=ignore_trailing,
ignore_errors=True,
+ ignore_unexpected=True,
)
return response
--
2.39.1

View File

@ -0,0 +1,173 @@
From ac6763f1018458835201b38cae848e4d261f3e5c Mon Sep 17 00:00:00 2001
From: Bob Halley <halley@dnspython.org>
Date: Fri, 16 Feb 2024 07:14:49 -0800
Subject: [PATCH] test IgnoreErrors
---
tests/test_query.py | 140 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 140 insertions(+)
diff --git a/tests/test_query.py b/tests/test_query.py
index 1116b2d..a47daa4 100644
--- a/tests/test_query.py
+++ b/tests/test_query.py
@@ -15,6 +15,7 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+import contextlib
import socket
import sys
import time
@@ -32,6 +33,7 @@ import dns.inet
import dns.message
import dns.name
import dns.query
+import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.tsigkeyring
@@ -659,3 +661,141 @@ class MiscTests(unittest.TestCase):
dns.query._matches_destination(
socket.AF_INET, ("10.0.0.1", 1234), ("10.0.0.1", 1235), False
)
+
+
+@contextlib.contextmanager
+def mock_udp_recv(wire1, from1, wire2, from2):
+ saved = dns.query._udp_recv
+ first_time = True
+
+ def mock(sock, max_size, expiration):
+ nonlocal first_time
+ if first_time:
+ 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 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_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)
--
2.39.1

View File

@ -0,0 +1,258 @@
From a1a998938b7370dae41784f8bc0a841dc2addba9 Mon Sep 17 00:00:00 2001
From: Bob Halley <halley@dnspython.org>
Date: Fri, 16 Feb 2024 08:46:24 -0800
Subject: [PATCH] Further improve CVE fix coverage to 100% for sync and async.
---
tests/test_async.py | 184 +++++++++++++++++++++++++++++++++++++++++++-
tests/test_query.py | 21 +++++
2 files changed, 204 insertions(+), 1 deletion(-)
diff --git a/tests/test_async.py b/tests/test_async.py
index 4ea2301..ba2078c 100644
--- a/tests/test_async.py
+++ b/tests/test_async.py
@@ -18,7 +18,6 @@
import asyncio
import random
import socket
-import sys
import time
import unittest
@@ -28,6 +27,7 @@ import dns.asyncresolver
import dns.message
import dns.name
import dns.query
+import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.resolver
@@ -664,3 +664,185 @@ try:
except ImportError:
pass
+
+
+class MockSock:
+ def __init__(self, wire1, from1, wire2, from2):
+ self.family = socket.AF_INET
+ self.first_time = True
+ self.wire1 = wire1
+ self.from1 = from1
+ self.wire2 = wire2
+ self.from2 = from2
+
+ async def sendto(self, data, where, timeout):
+ return len(data)
+
+ async def recvfrom(self, bufsize, expiration):
+ if self.first_time:
+ self.first_time = False
+ return self.wire1, self.from1
+ else:
+ return self.wire2, self.from2
+
+
+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()
+ dns.asyncbackend.set_default_backend("asyncio")
+
+ def async_run(self, afunc):
+ return asyncio.run(afunc())
+
+ async def mock_receive(
+ self,
+ wire1,
+ from1,
+ wire2,
+ from2,
+ ignore_unexpected=True,
+ ignore_errors=True,
+ ):
+ s = MockSock(wire1, from1, wire2, from2)
+ (r, when, _) = await dns.asyncquery.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)
+
+ def test_good_mock(self):
+ async def run():
+ await self.mock_receive(self.good_r_wire, ("127.0.0.1", 53), None, None)
+
+ self.async_run(run)
+
+ def test_bad_address(self):
+ async def run():
+ await self.mock_receive(
+ self.good_r_wire, ("127.0.0.2", 53), self.good_r_wire, ("127.0.0.1", 53)
+ )
+
+ self.async_run(run)
+
+ def test_bad_address_not_ignored(self):
+ async def abad():
+ await self.mock_receive(
+ self.good_r_wire,
+ ("127.0.0.2", 53),
+ self.good_r_wire,
+ ("127.0.0.1", 53),
+ ignore_unexpected=False,
+ )
+
+ def bad():
+ self.async_run(abad)
+
+ self.assertRaises(dns.query.UnexpectedSource, bad)
+
+ def test_not_response_not_ignored_udp_level(self):
+ async def abad():
+ bad_r = dns.message.make_response(self.q)
+ bad_r.id += 1
+ bad_r_wire = bad_r.to_wire()
+ s = MockSock(
+ bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53)
+ )
+ await dns.asyncquery.udp(self.good_r, "127.0.0.1", sock=s)
+
+ def bad():
+ self.async_run(abad)
+
+ self.assertRaises(dns.query.BadResponse, bad)
+
+ def test_bad_id(self):
+ async def run():
+ bad_r = dns.message.make_response(self.q)
+ bad_r.id += 1
+ bad_r_wire = bad_r.to_wire()
+ await self.mock_receive(
+ bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53)
+ )
+
+ self.async_run(run)
+
+ 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()
+
+ async def abad():
+ (r, wire) = await self.mock_receive(
+ bad_r_wire,
+ ("127.0.0.1", 53),
+ self.good_r_wire,
+ ("127.0.0.1", 53),
+ ignore_errors=False,
+ )
+
+ def bad():
+ self.async_run(abad)
+
+ self.assertRaises(AssertionError, bad)
+
+ def test_bad_wire(self):
+ async def run():
+ bad_r = dns.message.make_response(self.q)
+ bad_r.id += 1
+ bad_r_wire = bad_r.to_wire()
+ await self.mock_receive(
+ bad_r_wire[:10], ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53)
+ )
+
+ self.async_run(run)
+
+ 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()
+
+ async def abad():
+ await self.mock_receive(
+ bad_r_wire[:10],
+ ("127.0.0.1", 53),
+ self.good_r_wire,
+ ("127.0.0.1", 53),
+ ignore_errors=False,
+ )
+
+ def bad():
+ self.async_run(abad)
+
+ self.assertRaises(dns.message.ShortHeader, bad)
+
+ def test_trailing_wire(self):
+ async def run():
+ wire = self.good_r_wire + b"abcd"
+ await self.mock_receive(
+ wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53)
+ )
+
+ self.async_run(run)
+
+ def test_trailing_wire_not_ignored(self):
+ wire = self.good_r_wire + b"abcd"
+
+ async def abad():
+ await self.mock_receive(
+ wire,
+ ("127.0.0.1", 53),
+ self.good_r_wire,
+ ("127.0.0.1", 53),
+ ignore_errors=False,
+ )
+
+ def bad():
+ self.async_run(abad)
+
+ self.assertRaises(dns.message.TrailingJunk, bad)
diff --git a/tests/test_query.py b/tests/test_query.py
index a47daa4..1039a14 100644
--- a/tests/test_query.py
+++ b/tests/test_query.py
@@ -683,6 +683,14 @@ def mock_udp_recv(wire1, from1, wire2, from2):
dns.query._udp_recv = saved
+class MockSock:
+ def __init__(self):
+ self.family = socket.AF_INET
+
+ def sendto(self, data, where):
+ return len(data)
+
+
class IgnoreErrors(unittest.TestCase):
def setUp(self):
self.q = dns.message.make_query("example.", "A")
@@ -758,6 +766,19 @@ class IgnoreErrors(unittest.TestCase):
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
--
2.39.1

View File

@ -0,0 +1,141 @@
From adfc942725bd36d28ec53f7e5480ace9eb543bd8 Mon Sep 17 00:00:00 2001
From: Bob Halley <halley@dnspython.org>
Date: Thu, 14 Dec 2023 18:04:39 -0800
Subject: [PATCH] Ensure asyncio datagram sockets on windows have had a bind()
before recvfrom().
The fix for [#637] erroneously concluded that that windows asyncio
needed connected datagram sockets, but subsequent further
investation showed that the actual problem was that windows wants
an unconnected datagram socket to be bound before recvfrom is called.
Linux autobinds in this case to the wildcard address and port, so
that's why we didn't see any problems there. We now ensure that
the source is bound.
---
dns/_asyncio_backend.py | 13 ++++++-------
tests/test_async.py | 25 +++++--------------------
2 files changed, 11 insertions(+), 27 deletions(-)
diff --git a/dns/_asyncio_backend.py b/dns/_asyncio_backend.py
index 2631228..7d4d1b5 100644
--- a/dns/_asyncio_backend.py
+++ b/dns/_asyncio_backend.py
@@ -8,6 +8,7 @@ import sys
import dns._asyncbackend
import dns.exception
+import dns.inet
_is_win32 = sys.platform == "win32"
@@ -224,14 +225,12 @@ class Backend(dns._asyncbackend.Backend):
ssl_context=None,
server_hostname=None,
):
- if destination is None and socktype == socket.SOCK_DGRAM and _is_win32:
- raise NotImplementedError(
- "destinationless datagram sockets "
- "are not supported by asyncio "
- "on Windows"
- )
loop = _get_running_loop()
if socktype == socket.SOCK_DGRAM:
+ if _is_win32 and source is None:
+ # Win32 wants explicit binding before recvfrom(). This is the
+ # proper fix for [#637].
+ source = (dns.inet.any_for_af(af), 0)
transport, protocol = await loop.create_datagram_endpoint(
_DatagramProtocol,
source,
@@ -266,7 +265,7 @@ class Backend(dns._asyncbackend.Backend):
await asyncio.sleep(interval)
def datagram_connection_required(self):
- return _is_win32
+ return False
def get_transport_class(self):
return _HTTPTransport
diff --git a/tests/test_async.py b/tests/test_async.py
index d0f977a..ac32431 100644
--- a/tests/test_async.py
+++ b/tests/test_async.py
@@ -171,8 +171,6 @@ class MiscQuery(unittest.TestCase):
@unittest.skipIf(not tests.util.is_internet_reachable(), "Internet not reachable")
class AsyncTests(unittest.TestCase):
- connect_udp = sys.platform == "win32"
-
def setUp(self):
self.backend = dns.asyncbackend.set_default_backend("asyncio")
@@ -327,12 +325,12 @@ class AsyncTests(unittest.TestCase):
qname = dns.name.from_text("dns.google.")
async def run():
- if self.connect_udp:
- dtuple = (address, 53)
- else:
- dtuple = None
async with await self.backend.make_socket(
- dns.inet.af_for_address(address), socket.SOCK_DGRAM, 0, None, dtuple
+ dns.inet.af_for_address(address),
+ socket.SOCK_DGRAM,
+ 0,
+ None,
+ None,
) as s:
q = dns.message.make_query(qname, dns.rdatatype.A)
return await dns.asyncquery.udp(q, address, sock=s, timeout=2)
@@ -485,9 +483,6 @@ class AsyncTests(unittest.TestCase):
self.assertFalse(tcp)
def testUDPReceiveQuery(self):
- if self.connect_udp:
- self.skipTest("test needs connectionless sockets")
-
async def run():
async with await self.backend.make_socket(
socket.AF_INET, socket.SOCK_DGRAM, source=("127.0.0.1", 0)
@@ -509,9 +504,6 @@ class AsyncTests(unittest.TestCase):
self.assertEqual(sender_address, recv_address)
def testUDPReceiveTimeout(self):
- if self.connect_udp:
- self.skipTest("test needs connectionless sockets")
-
async def arun():
async with await self.backend.make_socket(
socket.AF_INET, socket.SOCK_DGRAM, 0, ("127.0.0.1", 0)
@@ -616,8 +608,6 @@ class AsyncTests(unittest.TestCase):
@unittest.skipIf(not tests.util.is_internet_reachable(), "Internet not reachable")
class AsyncioOnlyTests(unittest.TestCase):
- connect_udp = sys.platform == "win32"
-
def setUp(self):
self.backend = dns.asyncbackend.set_default_backend("asyncio")
@@ -625,9 +615,6 @@ class AsyncioOnlyTests(unittest.TestCase):
return asyncio.run(afunc())
def testUseAfterTimeout(self):
- if self.connect_udp:
- self.skipTest("test needs connectionless sockets")
-
# Test #843 fix.
async def run():
qname = dns.name.from_text("dns.google")
@@ -678,8 +665,6 @@ try:
return trio.run(afunc)
class TrioAsyncTests(AsyncTests):
- connect_udp = False
-
def setUp(self):
self.backend = dns.asyncbackend.set_default_backend("trio")
--
2.39.1

View File

@ -0,0 +1,229 @@
From 2ab3d1628c9ae0545e225522b3b445c3478dc6ad Mon Sep 17 00:00:00 2001
From: Bob Halley <halley@dnspython.org>
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
---
dns/asyncquery.py | 10 ++++++++
dns/query.py | 14 +++++++++++
tests/test_async.py | 60 ++++++++++++++++++++++++++++++++++++++++++++-
tests/test_query.py | 44 ++++++++++++++++++++++++++++++++-
4 files changed, 126 insertions(+), 2 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/tests/test_async.py b/tests/test_async.py
index ba2078cd..9373548d 100644
--- a/tests/test_async.py
+++ b/tests/test_async.py
@@ -705,7 +705,11 @@ async def mock_receive(
from2,
ignore_unexpected=True,
ignore_errors=True,
+ raise_on_truncation=False,
+ good_r=None,
):
+ if good_r is None:
+ good_r = self.good_r
s = MockSock(wire1, from1, wire2, from2)
(r, when, _) = await dns.asyncquery.receive_udp(
s,
@@ -713,9 +717,10 @@ async def mock_receive(
time.time() + 2,
ignore_unexpected=ignore_unexpected,
ignore_errors=ignore_errors,
+ raise_on_truncation=raise_on_truncation,
query=self.q,
)
- self.assertEqual(r, self.good_r)
+ self.assertEqual(r, good_r)
def test_good_mock(self):
async def run():
@@ -802,6 +807,59 @@ async def run():
self.async_run(run)
+ def test_good_wire_with_truncation_flag_and_no_truncation_raise(self):
+ async def run():
+ tc_r = dns.message.make_response(self.q)
+ tc_r.flags |= dns.flags.TC
+ tc_r_wire = tc_r.to_wire()
+ await self.mock_receive(
+ tc_r_wire, ("127.0.0.1", 53), None, None, good_r=tc_r
+ )
+
+ self.async_run(run)
+
+ def test_good_wire_with_truncation_flag_and_truncation_raise(self):
+ async def agood():
+ tc_r = dns.message.make_response(self.q)
+ tc_r.flags |= dns.flags.TC
+ tc_r_wire = tc_r.to_wire()
+ await self.mock_receive(
+ tc_r_wire, ("127.0.0.1", 53), None, None, raise_on_truncation=True
+ )
+
+ def good():
+ self.async_run(agood)
+
+ self.assertRaises(dns.message.Truncated, good)
+
+ def test_wrong_id_wire_with_truncation_flag_and_no_truncation_raise(self):
+ async def run():
+ bad_r = dns.message.make_response(self.q)
+ bad_r.id += 1
+ bad_r.flags |= dns.flags.TC
+ bad_r_wire = bad_r.to_wire()
+ await self.mock_receive(
+ bad_r_wire, ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53)
+ )
+
+ self.async_run(run)
+
+ def test_wrong_id_wire_with_truncation_flag_and_truncation_raise(self):
+ async def run():
+ bad_r = dns.message.make_response(self.q)
+ bad_r.id += 1
+ bad_r.flags |= dns.flags.TC
+ bad_r_wire = bad_r.to_wire()
+ await self.mock_receive(
+ bad_r_wire,
+ ("127.0.0.1", 53),
+ self.good_r_wire,
+ ("127.0.0.1", 53),
+ raise_on_truncation=True,
+ )
+
+ self.async_run(run)
+
def test_bad_wire_not_ignored(self):
bad_r = dns.message.make_response(self.q)
bad_r.id += 1
diff --git a/tests/test_query.py b/tests/test_query.py
index 1039a14e..62007e85 100644
--- a/tests/test_query.py
+++ b/tests/test_query.py
@@ -29,6 +29,7 @@
have_ssl = False
import dns.exception
+import dns.flags
import dns.inet
import dns.message
import dns.name
@@ -706,7 +707,11 @@ def mock_receive(
from2,
ignore_unexpected=True,
ignore_errors=True,
+ raise_on_truncation=False,
+ good_r=None,
):
+ if good_r is None:
+ good_r = self.good_r
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
with mock_udp_recv(wire1, from1, wire2, from2):
@@ -716,9 +721,10 @@ def mock_receive(
time.time() + 2,
ignore_unexpected=ignore_unexpected,
ignore_errors=ignore_errors,
+ raise_on_truncation=raise_on_truncation,
query=self.q,
)
- self.assertEqual(r, self.good_r)
+ self.assertEqual(r, good_r)
finally:
s.close()
@@ -787,6 +793,42 @@ def test_bad_wire(self):
bad_r_wire[:10], ("127.0.0.1", 53), self.good_r_wire, ("127.0.0.1", 53)
)
+ def test_good_wire_with_truncation_flag_and_no_truncation_raise(self):
+ tc_r = dns.message.make_response(self.q)
+ tc_r.flags |= dns.flags.TC
+ tc_r_wire = tc_r.to_wire()
+ self.mock_receive(tc_r_wire, ("127.0.0.1", 53), None, None, good_r=tc_r)
+
+ def test_good_wire_with_truncation_flag_and_truncation_raise(self):
+ def good():
+ tc_r = dns.message.make_response(self.q)
+ tc_r.flags |= dns.flags.TC
+ tc_r_wire = tc_r.to_wire()
+ self.mock_receive(
+ tc_r_wire, ("127.0.0.1", 53), None, None, raise_on_truncation=True
+ )
+
+ self.assertRaises(dns.message.Truncated, good)
+
+ def test_wrong_id_wire_with_truncation_flag_and_no_truncation_raise(self):
+ bad_r = dns.message.make_response(self.q)
+ bad_r.id += 1
+ bad_r.flags |= dns.flags.TC
+ 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_wrong_id_wire_with_truncation_flag_and_truncation_raise(self):
+ bad_r = dns.message.make_response(self.q)
+ bad_r.id += 1
+ bad_r.flags |= dns.flags.TC
+ 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),
+ raise_on_truncation=True
+ )
+
def test_bad_wire_not_ignored(self):
bad_r = dns.message.make_response(self.q)
bad_r.id += 1

View File

@ -14,11 +14,19 @@ messages, names, and records.
Name: python-dns Name: python-dns
Summary: %{sum} Summary: %{sum}
Version: 2.4.2 Version: 2.4.2
Release: 1 Release: 2
License: ISC and MIT License: ISC and MIT
URL: http://www.dnspython.org/ URL: http://www.dnspython.org/
Source0: https://github.com/rthalley/dnspython/archive/v%{version}/dnspython-%{version}.tar.gz Source0: https://github.com/rthalley/dnspython/archive/v%{version}/dnspython-%{version}.tar.gz
# fix CVE-2023-29483
Patch0001: 0001-Address-DoS-via-the-Tudoor-mechanism-CVE-2023-29483-.patch
Patch0002: 0002-For-the-Tudoor-fix-we-also-need-the-UDP-nameserver-t.patch
Patch0003: 0003-test-IgnoreErrors.patch
Patch0004: 0004-Further-improve-CVE-fix-coverage-to-100-for-sync-and.patch
Patch0005: 0005-Ensure-asyncio-datagram-sockets-on-windows-have-had-.patch
Patch0006: 0006-The-Tudoor-fix-should-not-eat-valid-Truncated-except.patch
BuildArch: noarch BuildArch: noarch
BuildRequires: python3-devel python3-setuptools python3-cryptography BuildRequires: python3-devel python3-setuptools python3-cryptography
@ -60,6 +68,9 @@ pytest
%doc examples %doc examples
%changelog %changelog
* Thu Apr 18 2024 wangguochun <wangguochun@kylinos.cn> - 2.4.2-2
- fix CVE-2023-29483
* Tue Dec 26 2023 gaihuiying <eaglegai@163.com> - 2.4.2-1 * Tue Dec 26 2023 gaihuiying <eaglegai@163.com> - 2.4.2-1
- update to 2.4.2 - update to 2.4.2