From a6fecdd8188fca45a75ffd1cf83b783d77e7e856 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Fri, 11 Sep 2026 11:39:16 +0200 Subject: [PATCH] Icon-Fetch: echten Bildtyp per Magic Bytes erkennen statt Server-Header zu vertrauen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01VLk93dhDMrkPXefVdknMV9 --- app.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/app.py b/app.py index 44279d0..105da28 100644 --- a/app.py +++ b/app.py @@ -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" 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