Problem
_seen_logs: dict[str, int | tuple] = {} is a module-level dict that is written but never read-from-empty and never expired. _log_exception_debug keys it by exc_str = str(exc_info[1]) and stores the full sys.exc_info() triple as the value. The exception messages produced by the decoder embed attacker-controlled values: self.source (peer IP and ephemeral source port, which varies per packet), the byte offset, and the pointer link — for example f"DNS compression pointer at {off} points to {link} beyond packet from {self.source}" (line 446) and similar at lines 417, 437, 450, 454, 465, 469. Each distinct combination yields a new key, and the stored traceback keeps frame references whose locals include self.data (the entire raw packet, up to 8966 bytes). A LAN attacker sending malformed packets that vary in offset/link/source-port can cause the parser to retain a slow-growing memory footprint of ~9 KB per unique exception string — easily millions of distinct strings, MBs-to-GBs over time — and the dict is never bounded or trimmed.
Why This Matters
The same multicast attack surface as the recursion finding (any host on the local link) lets a low-rate attacker grow long-running zeroconf processes' memory until OOM, with no observable error-rate spike on the wire. Particularly impactful for embedded / always-on consumers (Home Assistant on a Raspberry Pi) where memory headroom is small.
Suggested Fix
Either (a) bound the dict (e.g. if len(_seen_logs) > 128: _seen_logs.clear() before insertion, or back it with functools.lru_cache semantics keyed on a stable signature), or (b) avoid keeping exc_info at all — store a sentinel like True instead so the bytes/traceback aren't retained:
if exc_str not in _seen_logs:
_seen_logs[exc_str] = True # do not retain exc_info / traceback
log_exc_info = True
Combine with (c) normalizing the exception strings so peer IP/port and packet offsets are not part of the dedup key — e.g. format messages with a fixed template and pass dynamic values as logger args.
Details
|
|
| Severity |
🟡 Medium |
| Category |
dos |
| Location |
src/zeroconf/_protocol/incoming.py:66-67, 183-192 |
| Effort |
⚡ Quick fix |
🤖 Created by Kōan from audit session
Problem
_seen_logs: dict[str, int | tuple] = {}is a module-level dict that is written but never read-from-empty and never expired._log_exception_debugkeys it byexc_str = str(exc_info[1])and stores the fullsys.exc_info()triple as the value. The exception messages produced by the decoder embed attacker-controlled values:self.source(peer IP and ephemeral source port, which varies per packet), the byteoffset, and the pointerlink— for examplef"DNS compression pointer at {off} points to {link} beyond packet from {self.source}"(line 446) and similar at lines 417, 437, 450, 454, 465, 469. Each distinct combination yields a new key, and the stored traceback keeps frame references whose locals includeself.data(the entire raw packet, up to 8966 bytes). A LAN attacker sending malformed packets that vary in offset/link/source-port can cause the parser to retain a slow-growing memory footprint of ~9 KB per unique exception string — easily millions of distinct strings, MBs-to-GBs over time — and the dict is never bounded or trimmed.Why This Matters
The same multicast attack surface as the recursion finding (any host on the local link) lets a low-rate attacker grow long-running zeroconf processes' memory until OOM, with no observable error-rate spike on the wire. Particularly impactful for embedded / always-on consumers (Home Assistant on a Raspberry Pi) where memory headroom is small.
Suggested Fix
Either (a) bound the dict (e.g.
if len(_seen_logs) > 128: _seen_logs.clear()before insertion, or back it withfunctools.lru_cachesemantics keyed on a stable signature), or (b) avoid keepingexc_infoat all — store a sentinel likeTrueinstead so the bytes/traceback aren't retained:Combine with (c) normalizing the exception strings so peer IP/port and packet offsets are not part of the dedup key — e.g. format messages with a fixed template and pass dynamic values as
loggerargs.Details
src/zeroconf/_protocol/incoming.py:66-67, 183-192🤖 Created by Kōan from audit session