Zurück zu den Artikeln Ausgabe #1  / 

Eine statische Seite härten

Diese Seite hat kein Backend, keinen Login und keine Datenbank. Angreifbar ist sie trotzdem. Hier steht, was schiefgehen konnte, was ich geändert habe und wie ich es getestet habe.

<script> schleich
Das ProblemJeder Text, der als HTML in die Seite gelangt, kann im Browser eines Besuchers Code ausführen.
<img onerror> javascript: <svg onload>
Der AngriffIch habe klassische XSS-Payloads in die eigenen Inhalte der Seite eingespeist, um zu sehen, was ausgeführt wird.
CSP blockiert.
Die LösungAlles als Text ausgeben und den Browser das mit CSP und Trusted Types durchsetzen lassen.

Warum auch eine statische Seite Sicherheit braucht

Man denkt leicht, eine statische Seite biete keine Angriffsfläche. Es gibt keinen Servercode zum Ausnutzen und keine Datenbank zum Auslesen. Aber die Seite führt in jedem Browser JavaScript aus, und genau dort entsteht Cross-Site-Scripting (XSS).

Meine Seite baut Teile der Seite aus Daten: Kenntnisse, Zertifikate und die Liste der Artikel. Die erste Version fügte diese Daten mit innerHTML ein. Das ist unproblematisch, solange ich alle Daten selbst schreibe, aber ein einziger schlechter Titel oder Link, den ich irgendwo kopiere, würde für jeden, der die Seite öffnet, als Code laufen.

Was schiefgehen konnte

  • XSS über Inhalte: ein Artikeltitel wie <img src=x onerror=...> läuft, sobald die Seite ihn darstellt.
  • Bösartige Links: eine javascript:- oder data:-URL in einem Link führt beim Klick Code aus.
  • Code von Dritten: Schriften oder Skripte von fremden Domains können sich ändern, Besucher verfolgen oder kompromittiert sein.
  • Reverse Tabnabbing: eine in einem neuen Tab geöffnete Seite kann meinen ursprünglichen Tab umleiten.

Was ich geändert habe

1. Inhalte sind immer Text, nie HTML. Jedes Element wird mit createElement gebaut und mit textContent gefüllt. Der Browser behandelt die Daten als Text, Markup darin wird also angezeigt, nicht ausgeführt.

2. Eine strenge Content Security Policy. Der Browser lädt Skripte, Stile, Schriften und Bilder nur von meiner eigenen Seite. Inline-Skripte, eval, Plugins und Formulare sind blockiert.

default-src 'none'; script-src 'self'; style-src 'self';
font-src 'self'; img-src 'self'; connect-src 'self';
object-src 'none'; base-uri 'none'; form-action 'none';
require-trusted-types-for 'script'; trusted-types 'none'

3. Trusted Types. Die letzten beiden Zeilen der Policy lassen den Browser jede rohe HTML-Zuweisung verweigern, etwa innerHTML. Selbst wenn ich das alte Muster später versehentlich wieder einbaue, gibt es einen Fehler statt einer Sicherheitslücke.

4. Eine Allowlist für URLs. Jeder Link läuft durch eine Funktion, bevor er auf die Seite kommt. Nur https:-Links und Pfade auf meiner eigenen Seite kommen durch.

function safeUrl(value) {
  const u = new URL(value, location.href);
  if (u.protocol === "https:") return u.href;
  if (u.origin === location.origin) return u.href;
  return null; // javascript:, data: und alles andere wird verworfen
}

5. Keine Dritten. Die Schriften liegen auf der Seite selbst, und die Seite stellt keine Anfragen an andere Domains. Es gibt keinen fremden Code, dem man trauen müsste, und kein Tracking der Besucher.

6. Die Kleinigkeiten. Ausgehende Links nutzen rel="noopener noreferrer", die Referrer-Policy ist no-referrer, und /.well-known/security.txt sagt, wie man ein Problem melden kann.

Wie ich getestet habe

Ich habe echte Payloads in die Daten der Seite eingebaut, sie lokal ausgeliefert und in Chromium mit aktiver Policy geladen.

PayloadWoErgebnis
<img src=x onerror=...>ArtikeltitelAls Text angezeigt
<script>...</script>Artikel-TagAls Text angezeigt
"><svg onload=...>ArtikelzusammenfassungAls Text angezeigt
javascript:...Artikel- und Lebenslauf-LinkLink entfernt
data:text/html,...Artikel-LinkLink entfernt
Inline-<script> im HTMLSeitenquelltextVon CSP blockiert
eval()Skript der SeiteVon CSP blockiert
Zuweisung an innerHTMLLive-SeiteVon Trusted Types blockiert

Keine der Payloads wurde ausgeführt. Die letzte Prüfung habe ich nach dem Ausrollen auf der Live-Seite wiederholt.

Was noch fehlt

GitHub Pages erlaubt keine eigenen HTTP-Antwort-Header. Manche Schutzmaßnahmen funktionieren nur als Header und nicht als <meta>-Tag: frame-ancestors gegen Clickjacking, Strict-Transport-Security und Permissions-Policy. Das Risiko ist gering, weil die Seite nichts bietet, wozu man jemanden per Klick-Trick verleiten könnte, aber es bleibt eine Lücke.

Die Lösung ist, die Seite von einem Ort auszuliefern, an dem ich die Header kontrolliere. Das ist die nächste Ausgabe.