Offen für neue Projekte Accra, Ghana
Alle Schriften

Cybersicherheit · Authentifizierung · OAuth · Mindestprivilegien

Die Hintertür sichern: Wie ich den Admin-Login meiner Website gesichert habe

Wer eine Website und deren Admin-Panel kontrolliert, kontrolliert auch, was jeder Besucher sieht und ausführt. Hier ist, wie der Admin für diese Seite funktioniert, ohne dass ein Server gehackt werden muss, warum der Schlüssel nur eine Tür öffnet und welche Lücke in der Lieferkette ich noch schließen muss.

Von Festus Boamah · · 4 min Lesezeit

Die Vordertür einer Website ist der Teil, den Besucher sehen. Das Admin-Panel ist die Hintertür, und Angreifer wissen das. Wer das Admin-Panel kontrolliert, kontrolliert, was jeder Besucher liest, und im schlimmsten Fall, welcher Code in den Browsern der Besucher ausgeführt wird. Eine entstellte Startseite ist peinlich. Eine Startseite, die still und leise ein schädliches Skript an jeden Besucher ausliefert, ist viel schlimmer.

Als ich also einen Editor für diese Website hinzufügte, damit ich Beiträge von meinem Telefon aus veröffentlichen kann, ohne einen Code-Editor öffnen zu müssen, behandelte ich dessen Login als den sensibelsten Teil des gesamten Systems.

Diagramm des Admin-Anmeldeflusses: das CMS im Browser fragt einen kleinen Auth-Worker, um sich anzumelden, der Worker sendet den Benutzer zu GitHub mit einem zufälligen Statuswert, GitHub gibt einen Code zurück und der Worker gibt ein Token nur an festusboamah.com zurück. Jedes Speichern wird zu einem Git-Commit zum main.

Entscheidung 1: Kein Server, den man hacken kann

Die meisten Content-Management-Systeme laufen auf einem Server mit einer Datenbank von Benutzern und Passwörtern. Dieser Server wird zu einem Ziel: Ein veralteter Plugin, ein schwaches Passwort oder eine exponierte Datenbank können einem Angreifer die gesamte Website in die Hände spielen.

Diese Seite hat nichts dergleichen. Es handelt sich um eine statische Seite, die aus einem Git-Repository auf GitHub neu erstellt wird, sobald sich etwas ändert. Das Admin-Panel (Sveltia CMS) ist nur eine JavaScript-Anwendung, die in meinem Browser läuft und direkt mit der GitHub-API kommuniziert. Das Speichern eines Beitrags bedeutet, einen Commit durchzuführen.

Das verlagert die Sicherheitsfrage. Es gibt keinen Admin-Server, den man patchen muss, und keine Passwort-Datenbank, die ausgeliefert werden kann. Die eigentliche Frage lautet: Wer kann auf das Repository schreiben? Alles andere folgt aus der Antwort.

Entscheidung 2: Ein Schlüssel, der nur eine Tür öffnet

Um auf das Repository zu schreiben, benötigt das CMS ein Token von GitHub. Der einfache Weg, um eines zu erhalten, ist eine klassische OAuth-App mit dem repo Umfang. Aber dieses Token könnte jedes Repository lesen und schreiben, auf das mein Konto zugreifen kann, sowohl öffentlich als auch privat. Wenn es ausgeliefert wird, würde der Schaden weit über diese Website hinausgehen.

Stattdessen erfolgt die Anmeldung über eine GitHub-App, die:

  • auf dieses eine Repository installiert ist und
  • mit einer Berechtigung ausgestattet ist: Inhalte (lesen und schreiben). Sie kann keine Issues, Einstellungen, Workflows, andere Repositorys oder mein Konto berühren.

Dies ist das Prinzip der Mindestprivilegien: Jeder Schlüssel erhält die kleinstmögliche Macht, die dennoch den Job erledigt, damit der Schaden begrenzt ist, wenn (nicht wenn) etwas schiefgeht.

Entscheidung 3: Den Übergabe schützen

Der OAuth-Anmeldefluss läuft durch einen kleinen Cloudflare-Worker (den Open-Source-sveltia-cms-auth, der unverändert in mein Repository eingebunden ist, an einem festen Commit, damit ich genau weiß, welcher Code ausgeführt wird). Er hält das Client-Geheimnis der GitHub-App als verschlüsseltes Worker-Geheimnis, niemals im Code oder im Repository. Drei seiner Prüfungen sind erwähnenswert, da jede einen echten Angriff blockiert:

  • Domain-Zulassungsliste. Der Worker startet die Anmeldung nur für festusboamah.com. Jemand, der meine Admin-Seite auf seine eigene Domain kopiert, kann meinen Login-Helfer nicht verwenden.
  • CSRF-Schutz mit state. Bevor ich zu GitHub weitergeleitet werde, generiert der Worker einen zufälligen Wert, speichert ihn in einem HttpOnly, Secure-Cookie und übermittelt ihn an GitHub als OAuth-state-Parameter. Wenn GitHub mich zurücksendet, müssen die beiden Werte übereinstimmen. Dies verhindert, dass ein Angreifer meinen Browser dazu bringt, eine Anmeldung abzuschließen, die er gestartet hat.
  • Überprüfte Lieferung. Der Token wird mit postMessage an das CMS-Fenster übergeben. Der Worker gibt ihn nur frei, wenn die von dem Browser gemeldete Herkunft des anfragenden Fensters meine Domain ist. Diese Herkunft wird vom Browser und nicht von der Seite festgelegt, sodass sie nicht gefälscht werden kann.

Entscheidung 4: Jede Änderung zuordnen

Da jeder Speichervorgang ein Git-Commit zu main ist, hat jede Änderung an der Website einen Autor, einen Zeitstempel und eine genaue Differenz. In Bezug auf STRIDE wird damit die Abstreitbarkeit angegangen: Es gibt keine Änderungen auf der Website, ohne dass ein Eintrag vorhanden ist. Es bietet mir auch eine sofortige Wiederherstellung: Wenn jemals etwas Schlechtes veröffentlicht wird, kann ich den Commit zurücksetzen und Cloudflare Pages baut die saubere Version in etwa einer Minute neu.

Die Admin-Seiten erhalten auch ihre eigenen HTTP-Header: Eine separate Content-Security-Policy, X-Robots-Tag: noindex, damit Suchmaschinen sie nicht auflisten, und Cache-Control: no-store, damit nichts Sensibles zwischengespeichert wird.

Wo es immer noch schwach ist

  • Das CMS-Skript hat keine Integritätsprüfung. Die Admin-Seite lädt Sveltia CMS vom unpkg-CDN. Ich pinne eine exakte Version, die vor Überraschungs-Upgrades schützt, aber es gibt keinen Subresource-Integrity-Hash. Wenn das CDN jemals kompromittiert wird und eine modifizierte Datei bereitstellt, würde mein Browser sie mit einem gültigen GitHub-Token ausführen. Dies ist ein klassisches Software-Lieferketten-Risiko. Die Lösung besteht entweder in einem integrity-Hash im Skript-Tag oder in der Bereitstellung der Datei von meiner eigenen Domain.
  • Die Admin-CSP ist lockerer als die der Website. Um das CMS zu laden, erlaubt die Admin-Richtlinie Skripte von unpkg und inline-Skripte. Das bedeutet, dass, wenn jemals ein Injection-Bug in der Admin existiert, die Richtlinie ein Skript aus jedem Paket auf unpkg nicht stoppen würde. Das Selbsthosting des CMS würde es mir auch ermöglichen, dies zu verschärfen.
  • Alles ruht auf einem GitHub-Konto. Die App beschränkt, was ein Token tun kann, aber wer mein GitHub-Konto kontrolliert, kontrolliert die Website. Das macht eine starke Zwei-Faktor-Authentifizierung auf diesem Konto (idealerweise ein Passkey oder ein Sicherheitsschlüssel anstelle von SMS) zur wichtigsten Kontrolle in diesem gesamten Design, obwohl sie nicht im Code enthalten ist.

Die Lektion

Die nützlichste Sicherheitsentscheidung hier war nicht eine clevere Prüfung. Es war die Entfernung des Servers, dann die Verengung des einen verbleibenden Schlüssels auf das engste Mögliche. Bevor Verteidigungen hinzugefügt werden, sollte immer gefragt werden: Was kann ich entfernen, damit es weniger zu verteidigen gibt?

Haben Sie etwas entdeckt, was ich übersehen habe? Sagen Sie es mir.

Festus Boamah

Zusammenarbeiten

Lass uns etwas Bedeutendes bauen.

Ein Produkt, das gebaut, eine Marke, die geformt, oder ein Kurs, der gehalten werden soll? Ich freue mich, davon zu hören.