Leaflet + Stadia Maps hinter strikter CSP: zwei Stolpersteine

Kategorie: Technik

Schlagwörter: leaflet, maps, csp, caddy, astro, security


Für meine Foto-Sektion wollte ich eine Weltkarte haben, auf der alle Aufnahmeorte aus den EXIF-Koordinaten der Fotos eingezeichnet sind. Technisch unspektakulär: Leaflet plus leaflet.markercluster, Tiles von Stadia Maps, Marker aus den vorhandenen Sidecar-JSONs der Fotos. Astro rendert die Seite statisch, das Client-Script baut die Karte beim Load auf.

Lokal lief das auf Anhieb. In Produktion kam nichts — nur eine graue Fläche mit den Markern. Zwei unabhängige Probleme, beide mit den Security-Headern aus meiner Caddy-Config zu tun.

Problem 1: img-src 'self' blockt die Tiles

Die CSP in der Caddy-Config war bewusst restriktiv:

Content-Security-Policy "default-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; font-src 'self'; connect-src 'self'; ..."

Leaflet lädt Tiles als ganz normale <img>-Elemente — also schlägt die Direktive img-src 'self' data: unbarmherzig zu, sobald der Host nicht die eigene Domain ist.

Fix: Den Stadia-Maps-Host explizit in die img-src-Direktive aufnehmen. Bei mir liegt die Site-Config unter /etc/caddy/sites/adrian-altner.de.caddy:

- img-src 'self' data:;
+ img-src 'self' data: https://tiles-eu.stadiamaps.com;

connect-src muss nicht angefasst werden — Leaflet benutzt keine fetch- oder XHR-Requests für die Tiles, nur <img>-Tags.

Ablauf zum Übernehmen (Backup → Validate → Reload):

sudo cp /etc/caddy/sites/adrian-altner.de.caddy \
        /etc/caddy/sites/adrian-altner.de.caddy.pre-csp-tiles
sudo vim /etc/caddy/sites/adrian-altner.de.caddy   # img-src-Zeile patchen
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl reload caddy

caddy validate parst die Config komplett durch, ohne den laufenden Dienst anzufassen — Tippfehler fallen hier auf, nicht erst beim Reload.

Problem 2: Referrer-Policy "no-referrer" produziert 401

Nach dem Caddy-Reload lud die Karte — aber die Tiles zeigten nur noch QR-Codes und “401 Error · Invalid Authentication · Learn more at docs.stadiamaps.com/authentication”. Stadia Maps nutzt für die Domain-basierte Authentifizierung den Referer-Header, um zu prüfen, von welchem Host der Request kommt. Genau diesen Header hatte ich aber mit Referrer-Policy "no-referrer" global abgeschaltet.

Die globale Policy aus DSGVO-Gründen aufzuweichen wollte ich nicht. Also brauchte es eine Ausnahme nur für die Tile-Requests. Der Weg darüber: das referrerpolicy-Attribut direkt am jeweiligen <img>-Element — das überschreibt die Dokumenten-Policy.

Leaflets TileLayer unterstützt genau das seit 1.9:

L.tileLayer('https://tiles-eu.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png', {
  attribution: '…',
  maxZoom: 20,
  referrerPolicy: 'origin',
}).addTo(map);

'origin' sendet nur den Origin (also https://adrian-altner.de), keine Pfade, keine Query-Parameter — genug für Stadia Maps, um die Domain zu verifizieren, aber minimal im Sinne der Referrer-Policy.

Was ich mir mitnehme

  • CSP-Ausnahmen für Third-Party-Maps sind unvermeidbar. Sobald Kartendienste Tiles oder Glyphs ausliefern, musst du deren Host in img-src (und bei WebGL-basierten Libs wie MapLibre auch in worker-src und connect-src) freigeben.
  • Referrer-Policy und Referer-basierte Auth beißen sich. Services wie Stadia Maps, Mapbox oder Google Maps nutzen den Referer als Domain-Nachweis. Eine globale no-referrer-Policy zerstört das stillschweigend. Per-Element-Overrides sind der saubere Mittelweg.
  • Lokale Entwicklung ist keine Referenz. Stadia Maps (und viele andere) erlauben localhost und 127.0.0.1 ohne Authentifizierung — der Bug taucht erst in Produktion auf. Teste CSP-relevante Dinge nicht nur im Dev-Server.