Problem
QueryHandler.async_response calls self.question_history.add_question_at_time(question, now, known_answers_set) for every non-unicast question in every incoming query the registry can match. QuestionHistory._history is a plain dict[DNSQuestion, (time, known_answers)] with no size cap; the only eviction is async_expire, called from the engine's periodic cleanup at _CACHE_CLEANUP_INTERVAL = 10 seconds. A LAN peer that streams queries with synthetic-but-matching service names — each carrying many distinct questions — can grow _history (and the retained known_answers sets) for up to ~10 s before the next cleanup tick fires, easily reaching hundreds of MB at line-rate.
Why This Matters
Same threat model and same daemon (Home Assistant / IoT bridge) as the _deferred issue; this is a second independent vector that lets a malicious LAN device drive the process toward OOM. The 10 s cleanup interval is long enough for the burst to land before the periodic sweep runs, and add_question_at_time does not consult the size of the dict it's growing.
Suggested Fix
Add a hard cap on _history (e.g. a few thousand entries) and refuse to insert (or evict the oldest by insertion order — dict is ordered) when at cap. Optionally trigger an opportunistic async_expire(now) from inside add_question_at_time when len(self._history) crosses a high-water mark, instead of waiting for the 10 s cleanup timer. Track known_answers as a count or hashed digest if the full set is not needed for the suppression decision.
Details
|
|
| Severity |
🟡 Medium |
| Category |
dos |
| Location |
src/zeroconf/_history.py:34-77, src/zeroconf/_handlers/query_handler.py:354-356, src/zeroconf/_engine.py:129-144 |
| Effort |
⚡ Quick fix |
🤖 Created by Kōan from audit session
Problem
QueryHandler.async_responsecallsself.question_history.add_question_at_time(question, now, known_answers_set)for every non-unicast question in every incoming query the registry can match.QuestionHistory._historyis a plaindict[DNSQuestion, (time, known_answers)]with no size cap; the only eviction isasync_expire, called from the engine's periodic cleanup at_CACHE_CLEANUP_INTERVAL = 10seconds. A LAN peer that streams queries with synthetic-but-matching service names — each carrying many distinct questions — can grow_history(and the retainedknown_answerssets) for up to ~10 s before the next cleanup tick fires, easily reaching hundreds of MB at line-rate.Why This Matters
Same threat model and same daemon (Home Assistant / IoT bridge) as the
_deferredissue; this is a second independent vector that lets a malicious LAN device drive the process toward OOM. The 10 s cleanup interval is long enough for the burst to land before the periodic sweep runs, andadd_question_at_timedoes not consult the size of the dict it's growing.Suggested Fix
Add a hard cap on
_history(e.g. a few thousand entries) and refuse to insert (or evict the oldest by insertion order —dictis ordered) when at cap. Optionally trigger an opportunisticasync_expire(now)from insideadd_question_at_timewhenlen(self._history)crosses a high-water mark, instead of waiting for the 10 s cleanup timer. Trackknown_answersas a count or hashed digest if the full set is not needed for the suppression decision.Details
src/zeroconf/_history.py:34-77, src/zeroconf/_handlers/query_handler.py:354-356, src/zeroconf/_engine.py:129-144🤖 Created by Kōan from audit session