Icon-Fetch: echten Bildtyp per Magic Bytes erkennen statt Server-Header zu vertrauen

Live gefunden beim Icon-Backfill (Phase 6): learningequality.org liefert
sein Favicon mit Content-Type "image/svg+xml", tatsächlich sind die Bytes
ein PNG-in-ICO-Container - wäre im Browser als kaputtes Bild gelandet
(SVG-Parser auf Binärdaten). _erkanntes_bildformat() sniffed jetzt die
echten Magic Bytes (PNG/JPEG/GIF/ICO/WEBP/SVG-Erkennung), der HTTP-Header
dient nur noch als Fallback. Zentral in _fetch_url_bytes() gefixt, damit
sowohl Webapp- als auch Merkmal-Icons profitieren (gleicher Fetch-Pfad).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLk93dhDMrkPXefVdknMV9
This commit is contained in:
adminandClaude Sonnet 5 committed 2026-09-11 11:39:16 +02:00
1 parent 95946bd11c
commit a6fecdd818
1 file changed
+30 -1
+30 -1
View File
@@ -769,6 +769,35 @@ FAVICON_LINK_REGEX = re.compile(
FAVICON_HREF_REGEX = re.compile(r'href=["\']([^"\']+)["\']', re.IGNORECASE)
# Live gefunden (11.09.2026, Phase 6 Icon-Backfill): learningequality.org
# liefert sein Favicon mit Content-Type "image/svg+xml", tatsaechlich sind
# die Bytes ein PNG-in-ICO-Container - ein Browser haette das als kaputtes
# Bild angezeigt (SVG-Parser auf Binaerdaten). Server-Header sind nicht
# vertrauenswuerdig genug, um sie ungeprueft in icon_content_type zu
# uebernehmen - _erkanntes_bildformat() sniffed stattdessen die echten
# Magic Bytes, der Header dient nur noch als Fallback, falls die Signatur
# nichts Bekanntes ergibt (z.B. ein Format ausserhalb dieser Liste).
_BILD_SIGNATUREN = [
(b"\x89PNG\r\n\x1a\n", "image/png"),
(b"\xff\xd8\xff", "image/jpeg"),
(b"GIF87a", "image/gif"),
(b"GIF89a", "image/gif"),
(b"\x00\x00\x01\x00", "image/x-icon"),
]
def _erkanntes_bildformat(data: bytes) -> str | None:
kopf = data[:16]
for signatur, mime in _BILD_SIGNATUREN:
if kopf.startswith(signatur):
return mime
if kopf.startswith(b"RIFF") and data[8:12] == b"WEBP":
return "image/webp"
if b"<svg" in data[:512]:
return "image/svg+xml"
return None
def _fetch_url_bytes(url: str, max_bytes: int) -> tuple[bytes, str] | None:
"""Best-effort GET mit Groessen-/Zeitlimit. None bei jedem Fehlschlag -
Aufrufer entscheiden ueber Fallback, kein Exception-Handling noetig."""
@@ -782,7 +811,7 @@ def _fetch_url_bytes(url: str, max_bytes: int) -> tuple[bytes, str] | None:
data = response.read(max_bytes + 1)
if len(data) > max_bytes:
return None
return data, content_type
return data, _erkanntes_bildformat(data) or content_type
except (urllib.error.URLError, TimeoutError, ValueError):
return None