VYPR
Medium severity6.5GHSA Advisory· Published May 29, 2026· Updated May 29, 2026

zeroconf has unbounded recursion in DNS compression-pointer decoder that allows LAN-local denial of service

CVE-2026-47180

Description

Impact

DNSIncoming._decode_labels_at_offset recurses once per DNS-name compression pointer (RFC 1035 §4.1.4). Pointer cycles and label counts were capped, but the chain length of unique forward pointers was not. A single ~3 kB mDNS packet carrying ~1500 chained pointers drives the recursion past CPython's default limit, and RecursionError was not listed in DECODE_EXCEPTIONS, so it escaped DNSIncoming.__init__ and was logged by asyncio's default exception handler.

Any unauthenticated host on the local link (UDP/5353, 224.0.0.251 / ff02::fb) can degrade the mDNS listener; that includes a guest on the same Wi-Fi, a compromised IoT device, or a container on a shared bridge. Replaying at a few hertz produces sustained CPU burn and log flooding, and mDNS-dependent features (HomeKit, Chromecast/Matter, AirPlay, printers) degrade while the attack is in flight.

Patches

Fixed in zeroconf 0.149.5 (PR #1719). Upgrade to >= 0.149.5.

Workarounds

There is no in-process workaround; upgrading is the fix. Otherwise, restrict mDNS (UDP/5353) to trusted Layer-2 segments via AP client isolation, guest-network separation, or host firewall rules.

### Resources - PR #1719, fix - Issue #1713, public tracking issue - RFC 1035 §4.1.4, RFC 6762, CWE-674

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Unbounded recursion in python-zeroconf's DNS compression-pointer decoder allows unauthenticated local-link attackers to cause denial of service via crafted mDNS packets.

Vulnerability

DNSIncoming._decode_labels_at_offset in python-zeroconf recurses once per DNS-name compression pointer (RFC 1035 §4.1.4). While pointer cycles and label counts were capped, the chain length of unique forward pointers was not. A single ~3 kB mDNS packet carrying ~1500 chained pointers drives recursion past CPython's default limit (1000). RecursionError was not listed in DECODE_EXCEPTIONS, so it escapes DNSIncoming.__init__ and is logged by asyncio's default exception handler. All versions prior to 0.149.5 are affected [1][2].

Exploitation

An unauthenticated host on the same local link (UDP/5353, multicast addresses 224.0.0.251 / ff02::fb) can send a single crafted mDNS packet with chained compression pointers. No authentication or user interaction is required. Replaying the packet at a few hertz produces sustained CPU burn and log flooding [2].

Impact

Successful exploitation causes denial of service: the mDNS listener degrades, and mDNS-dependent features (HomeKit, Chromecast/Matter, AirPlay, printers) become unavailable while the attack is in flight. No code execution or data disclosure is possible [2].

Mitigation

The vulnerability is fixed in zeroconf version 0.149.5 (PR #1719) [1]. Upgrade to >= 0.149.5. There is no in-process workaround. Network-level mitigation includes restricting mDNS (UDP/5353) to trusted Layer-2 segments via AP client isolation, guest-network separation, or host firewall rules [2].

AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
f9e23592137f

fix: bound DNS compression-pointer chain depth in DNSIncoming (#1719)

https://github.com/python-zeroconf/python-zeroconfJ. Nick KostonMay 18, 2026via body-scan-shorthand
3 files changed · +33 5
  • src/zeroconf/_protocol/incoming.pxd+1 1 modified
    @@ -83,7 +83,7 @@ cdef class DNSIncoming:
             link_py_int=object,
             linked_labels=cython.list
         )
    -    cdef unsigned int _decode_labels_at_offset(self, unsigned int off, cython.list labels, cython.set seen_pointers)
    +    cdef unsigned int _decode_labels_at_offset(self, unsigned int off, cython.list labels, cython.set seen_pointers, unsigned int depth)
     
         @cython.locals(offset="unsigned int")
         cdef void _read_header(self)
    
  • src/zeroconf/_protocol/incoming.py+10 4 modified
    @@ -60,7 +60,7 @@
     MAX_DNS_LABELS = 128
     MAX_NAME_LENGTH = 253
     
    -DECODE_EXCEPTIONS = (IndexError, struct.error, IncomingDecodeError)
    +DECODE_EXCEPTIONS = (IndexError, struct.error, IncomingDecodeError, RecursionError)
     
     
     _seen_logs: dict[str, int | tuple] = {}
    @@ -409,7 +409,7 @@ def _read_name(self) -> str:
             labels: list[str] = []
             seen_pointers: set[int] = set()
             original_offset = self.offset
    -        self.offset = self._decode_labels_at_offset(original_offset, labels, seen_pointers)
    +        self.offset = self._decode_labels_at_offset(original_offset, labels, seen_pointers, 0)
             self._name_cache[original_offset] = labels
             name = ".".join(labels) + "."
             if len(name) > MAX_NAME_LENGTH:
    @@ -418,8 +418,14 @@ def _read_name(self) -> str:
                 )
             return name
     
    -    def _decode_labels_at_offset(self, off: _int, labels: list[str], seen_pointers: set[int]) -> int:
    +    def _decode_labels_at_offset(
    +        self, off: _int, labels: list[str], seen_pointers: set[int], depth: _int
    +    ) -> int:
             # This is a tight loop that is called frequently, small optimizations can make a difference.
    +        if depth > MAX_DNS_LABELS:
    +            raise IncomingDecodeError(
    +                f"DNS compression pointer chain exceeds {MAX_DNS_LABELS} at {off} from {self.source}"
    +            )
             view = self.view
             while off < self._data_len:
                 length = view[off]
    @@ -457,7 +463,7 @@ def _decode_labels_at_offset(self, off: _int, labels: list[str], seen_pointers:
                 if not linked_labels:
                     linked_labels = []
                     seen_pointers.add(link_py_int)
    -                self._decode_labels_at_offset(link, linked_labels, seen_pointers)
    +                self._decode_labels_at_offset(link, linked_labels, seen_pointers, depth + 1)
                     self._name_cache[link_py_int] = linked_labels
                 labels.extend(linked_labels)
                 if len(labels) > MAX_DNS_LABELS:
    
  • tests/test_protocol.py+22 0 modified
    @@ -1011,6 +1011,28 @@ def test_label_compression_attack():
         assert len(parsed.answers()) == 1
     
     
    +def test_dns_compression_pointer_chain_depth_attack() -> None:
    +    """Test our wire parser rejects deeply chained compression pointers without recursing."""
    +    # Build a packet with one question whose name is a 1500-deep chain of forward
    +    # compression pointers, ending in a root label. Each pointer is 2 bytes,
    +    # so chain length easily exceeds CPython's default recursion limit.
    +    header = b"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
    +    # Question at offset 12: pointer to offset 18 (past the question's type/class).
    +    question_name = bytes([0xC0, 18])
    +    question_type_class = b"\x00\x01\x00\x01"
    +    chain_depth = 1500
    +    chain = bytearray()
    +    for i in range(chain_depth):
    +        target = 18 + 2 * (i + 1)
    +        chain.append(0xC0 | (target >> 8))
    +        chain.append(target & 0xFF)
    +    chain.append(0x00)
    +    packet = header + question_name + question_type_class + bytes(chain)
    +    parsed = r.DNSIncoming(packet, ("1.2.3.4", 5353))
    +    assert parsed.valid is False
    +    assert parsed.questions == []
    +
    +
     def test_dns_compression_loop_attack():
         """Test our wire parser does not loop forever when dns compression is in a loop."""
         packet = (
    

Vulnerability mechanics

No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.

References

4

News mentions

0

No linked articles in our index yet.