fix: Katalog-SSID-Cache threadsicher, Sicherheitshinweis zu mDNS/MAC

scan() konnte den SSID-Cache filtern, während contains_ssid() aus einem
anderen Executor-Thread gleichzeitig hineinschrieb ("dictionary changed
size during iteration"). Ein Lock schützt jetzt beide Zugriffe.

README ergänzt: mDNS-Updates auf eine bekannte MAC laufen ungeprüft: Ein
Gerät, das dieselbe MAC ankündigt, übernimmt den Host-Eintrag und könnte
so an Anfragen mit Token gelangen (nur in einem bereits kompromittierten
Netz relevant).
This commit is contained in:
Michael Sauer
2026-09-16 17:40:21 +02:00
parent b8f3c94278
commit fc29b0befe
2 changed files with 43 additions and 8 deletions
+40 -8
View File
@@ -11,6 +11,7 @@ import hashlib
import logging
from pathlib import Path
import re
import threading
import zlib
_LOGGER = logging.getLogger(__name__)
@@ -132,6 +133,9 @@ class FirmwareCatalog:
self.images: tuple[FirmwareImage, ...] = ()
self._parsed: dict[tuple[str, int, int], FirmwareImage | None] = {}
self._ssid_hits: dict[tuple[str, int, int, str], bool] = {}
# scan() runs in one executor thread while contains_ssid() may run in another (e.g. an
# install's Wi-Fi re-check racing a periodic rescan): both read and mutate _ssid_hits.
self._ssid_hits_lock = threading.Lock()
def scan(self) -> tuple[FirmwareImage, ...]:
"""Blocking. Invalid files are logged once per (path, mtime, size)."""
@@ -161,7 +165,8 @@ class FirmwareCatalog:
if image is not None:
found.append(image)
self._parsed = {k: v for k, v in self._parsed.items() if k in seen}
self._ssid_hits = {k: v for k, v in self._ssid_hits.items() if k[:3] in seen}
with self._ssid_hits_lock:
self._ssid_hits = {k: v for k, v in self._ssid_hits.items() if k[:3] in seen}
self.images = tuple(found)
return self.images
@@ -184,13 +189,40 @@ class FirmwareCatalog:
except OSError, InvalidImageError:
return False
def verify_and_read(self, image: FirmwareImage) -> bytes | None:
"""Blocking. Like `verify`, but returns the exact bytes that were checked (or None).
Callers that need to act on the verified content (e.g. serve it, or scan it for an SSID)
must use these bytes rather than reading the file again afterwards - the file on disk can
change between the two reads.
"""
try:
stat = image.path.stat()
if (stat.st_mtime_ns, stat.st_size) != (image.mtime_ns, image.size):
return None
data = image.path.read_bytes()
except OSError:
return None
try:
if parse_trailer(data) != (image.device_type, image.version):
return None
except InvalidImageError:
return None
return data
def contains_ssid(self, image: FirmwareImage, ssid: str) -> bool:
"""Blocking, cached per (image file, ssid)."""
key = (str(image.path), image.mtime_ns, image.size, ssid)
if key not in self._ssid_hits:
try:
data = image.path.read_bytes()
except OSError:
return False
self._ssid_hits[key] = contains_ssid(data, ssid)
return self._ssid_hits[key]
with self._ssid_hits_lock:
cached = key in self._ssid_hits
hit = self._ssid_hits.get(key)
if cached:
return bool(hit)
try:
data = image.path.read_bytes()
except OSError:
return False
hit = contains_ssid(data, ssid)
with self._ssid_hits_lock:
self._ssid_hits[key] = hit
return hit