CVE-2023-29483
Description
eventlet before 0.35.2, as used in dnspython before 2.6.0, allows remote attackers to interfere with DNS name resolution by quickly sending an invalid packet from the expected IP address and source port, aka a "TuDoor" attack. In other words, dnspython does not have the preferred behavior in which the DNS name resolution algorithm would proceed, within the full time window, in order to wait for a valid packet. NOTE: dnspython 2.6.0 is unusable for a different reason that was addressed in 2.6.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2023-29483, aka 'TuDoor', allows remote attackers to disrupt DNS resolution by sending an invalid packet from the expected IP and port, affecting dnspython before 2.6.0 and eventlet before 0.35.2.
Vulnerability
Description
The TuDoor vulnerability (CVE-2023-29483) is a flaw in the DNS name resolution algorithm when using eventlet before 0.35.2 and dnspython before 2.6.0. An attacker can interfere with DNS resolution by quickly sending an invalid packet from the expected IP address and source port, causing the resolver to accept the spoofed response and potentially fail over to another resolver or give up entirely [1][2].
Exploitation
The attack requires the attacker to know the source IP and port used by the resolver for a specific query. By sending a crafted invalid packet before the legitimate response arrives, the resolver processes the spoofed packet and may discard the valid response. No authentication is required, and the attacker can be remote as long as they can spoof packets from the resolver's expected peer [1][3].
Impact
Successful exploitation can lead to denial of service for DNS resolution, as the stub resolver may switch to a different resolver or timeout, preventing legitimate DNS queries from being answered. This could impact any application relying on dnspython or eventlet for DNS, potentially causing network disruptions [2].
Mitigation
The issue is fixed in dnspython 2.6.1 (note: 2.6.0 is unusable due to a separate bug that also truncated legitimate responses) and eventlet 0.35.2. Users should upgrade to these versions or later. Additionally, eventlet is now discouraged for new projects, and migration to asyncio is recommended [4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
eventletPyPI | < 0.35.2 | 0.35.2 |
dnspythonPyPI | < 2.6.1 | 2.6.1 |
Affected products
48- eventlet/dnspythondescription
- osv-coords47 versionspkg:apk/chainguard/kubeflow-pipelines-visualization-serverpkg:apk/chainguard/py3.10-dnspythonpkg:apk/chainguard/py3.11-dnspythonpkg:apk/chainguard/py3.12-dnspythonpkg:apk/chainguard/py3.13-dnspythonpkg:apk/chainguard/py3-dnspythonpkg:apk/chainguard/py3-supported-dnspythonpkg:apk/wolfi/kubeflow-pipelines-visualization-serverpkg:apk/wolfi/py3.10-dnspythonpkg:apk/wolfi/py3.11-dnspythonpkg:apk/wolfi/py3.12-dnspythonpkg:apk/wolfi/py3.13-dnspythonpkg:apk/wolfi/py3-dnspythonpkg:apk/wolfi/py3-supported-dnspythonpkg:pypi/dnspythonpkg:pypi/eventletpkg:rpm/almalinux/python3-dnspkg:rpm/opensuse/python-dnspython&distro=openSUSE%20Leap%2015.5pkg:rpm/opensuse/python-dnspython&distro=openSUSE%20Leap%2015.6pkg:rpm/opensuse/python-dnspython&distro=openSUSE%20Leap%20Micro%205.5pkg:rpm/opensuse/python-dnspython&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/python-eventlet&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/python-pymongo&distro=openSUSE%20Tumbleweedpkg:rpm/suse/python-dnspython&distro=SUSE%20Enterprise%20Storage%207.1pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015%20SP2-LTSSpkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015%20SP3-LTSSpkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015%20SP4-ESPOSpkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20High%20Performance%20Computing%2015%20SP4-LTSSpkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Micro%205.3pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Micro%205.4pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Micro%205.5pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Basesystem%2015%20SP5pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Basesystem%2015%20SP6pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Package%20Hub%2015%20SP5pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Package%20Hub%2015%20SP6pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Public%20Cloud%2012pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Python%203%2015%20SP5pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Python%203%2015%20SP6pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Server%2015%20SP2-LTSSpkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Server%2015%20SP3-LTSSpkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Server%2015%20SP4-LTSSpkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20Applications%2015%20SP2pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20Applications%2015%20SP3pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Enterprise%20Server%20for%20SAP%20Applications%2015%20SP4pkg:rpm/suse/python-dnspython&distro=SUSE%20Linux%20Micro%206.0pkg:rpm/suse/python-dnspython&distro=SUSE%20Manager%20Proxy%204.3pkg:rpm/suse/python-dnspython&distro=SUSE%20Manager%20Server%204.3
< 2.2.0-r0+ 46 more
- (no CPE)range: < 2.2.0-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.2.0-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.6.1-r0
- (no CPE)range: < 2.6.1
- (no CPE)range: < 0.35.2
- (no CPE)range: < 1.15.0-12.el8_10
- (no CPE)range: < 2.3.0-150400.12.6.1
- (no CPE)range: < 2.3.0-150400.12.6.1
- (no CPE)range: < 1.15.0-150000.3.5.1
- (no CPE)range: < 2.6.1-1.1
- (no CPE)range: < 0.36.1-1.1
- (no CPE)range: < 4.16.0-1.1
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 1.15.0-150000.3.5.1
- (no CPE)range: < 1.15.0-150000.3.5.1
- (no CPE)range: < 1.15.0-150000.3.5.1
- (no CPE)range: < 1.15.0-150000.3.5.1
- (no CPE)range: < 1.15.0-150000.3.5.1
- (no CPE)range: < 1.15.0-150000.3.5.1
- (no CPE)range: < 1.15.0-150000.3.5.1
- (no CPE)range: < 1.12.0-9.13.1
- (no CPE)range: < 2.3.0-150400.12.6.1
- (no CPE)range: < 2.3.0-150400.12.6.1
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 2.4.2-2.1
- (no CPE)range: < 1.15.0-150000.3.10.2
- (no CPE)range: < 1.15.0-150000.3.10.2
Patches
251e3c4928d49Dnspython 2.6.1 - Address DoS via the Tudoor mechanism (CVE-2023-29483)
1 file changed · +38 −18
eventlet/support/greendns.py+38 −18 modified@@ -713,7 +713,7 @@ def _net_write(sock, data, expiration): def udp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53, af=None, 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): """coro friendly replacement for dns.query.udp Return the response obtained after sending a query via UDP. @@ -752,7 +752,10 @@ def udp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53, 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. - @type sock: socket.socket | None""" + @type sock: socket.socket | None + @param ignore_errors: if various format errors or response mismatches occur, + continue listening. + @type ignore_errors: bool""" wire = q.to_wire() if af is None: @@ -816,26 +819,43 @@ def udp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53, addr = from_address[0] addr = dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(addr)) from_address = (addr, from_address[1], from_address[2], from_address[3]) - if from_address == destination: + if from_address != destination: + if ignore_unexpected: + continue + else: + raise dns.query.UnexpectedSource( + 'got a response from %s instead of %s' + % (from_address, destination)) + try: + if _handle_raise_on_truncation: + r = dns.message.from_wire(wire, + keyring=q.keyring, + request_mac=q.mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + raise_on_truncation=raise_on_truncation) + else: + r = dns.message.from_wire(wire, + keyring=q.keyring, + request_mac=q.mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing) + if not q.is_response(r): + raise dns.query.BadResponse() break - if not ignore_unexpected: - raise dns.query.UnexpectedSource( - 'got a response from %s instead of %s' - % (from_address, destination)) + except dns.message.Truncated as e: + if ignore_errors and not q.is_response(e.message()): + continue + else: + raise + except Exception: + if ignore_errors: + continue + else: + raise finally: s.close() - if _handle_raise_on_truncation: - r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac, - one_rr_per_rrset=one_rr_per_rrset, - ignore_trailing=ignore_trailing, - raise_on_truncation=raise_on_truncation) - else: - r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac, - one_rr_per_rrset=one_rr_per_rrset, - ignore_trailing=ignore_trailing) - if not q.is_response(r): - raise dns.query.BadResponse() return r
0ea5ad0a4583The Tudoor fix should not eat valid Truncated exceptions [#1053] (#1054)
4 files changed · +126 −2
dns/asyncquery.py+10 −0 modified@@ -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
dns/query.py+14 −0 modified@@ -638,6 +638,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
tests/test_async.py+59 −1 modified@@ -705,17 +705,22 @@ 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, ("127.0.0.1", 53), 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
tests/test_query.py+43 −1 modified@@ -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
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
19- github.com/advisories/GHSA-3rq5-2g8h-59hcghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/NLRKR57IFVKQC2GCXZBFLCLBAWBWL3F6/mitrevendor-advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/VOHJOO3OM65UIUUUVDEXMCTXNM6LXZEH/mitrevendor-advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/X3BNSIK5NFYSAP53Y45GOCMOQHHDLGIF/mitrevendor-advisory
- nvd.nist.gov/vuln/detail/CVE-2023-29483ghsaADVISORY
- github.com/eventlet/eventlet/commit/51e3c4928d4938beb576eff34f3bf97e6e64e6b4ghsaWEB
- github.com/eventlet/eventlet/issues/913ghsaWEB
- github.com/eventlet/eventlet/releases/tag/v0.35.2ghsaWEB
- github.com/rthalley/dnspython/commit/0ea5ad0a4583e1f519b9bcc67cfac381230d9cf2ghsaWEB
- github.com/rthalley/dnspython/issues/1045ghsaWEB
- github.com/rthalley/dnspython/releases/tag/v2.6.0ghsaWEB
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/NLRKR57IFVKQC2GCXZBFLCLBAWBWL3F6ghsaWEB
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/VOHJOO3OM65UIUUUVDEXMCTXNM6LXZEHghsaWEB
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/X3BNSIK5NFYSAP53Y45GOCMOQHHDLGIFghsaWEB
- security.netapp.com/advisory/ntap-20240510-0001ghsaWEB
- security.snyk.io/vuln/SNYK-PYTHON-DNSPYTHON-6241713ghsaWEB
- www.dnspython.orgghsaWEB
- security.netapp.com/advisory/ntap-20240510-0001/mitre
- www.dnspython.orgmitre
News mentions
0No linked articles in our index yet.