Implementierung einer Content Security Policy (CSP) bei einer bestehenden Webanwendung am Beispiel Server-Daten - Schutz so, dass sogar Nutzer geschützt sind, deren Browser CSP nicht unterstützt
Eine Content Security Policy (CSP) ist ein mächtiges Schutzwerkzeug für Webanwendungen. Bevor der Webserver die eigentliche Seite ausliefert, teilt er per Header-Anweisung mit, daß Scripte und CSS-Dateien bsp. nur von lokalen Quellen und von Google erlaubt sind. Und daß der Browser alles an Inline-JavaScript ignorieren möge.
Eigentlich ist das eine witzige Anweisung: Der Server sagt damit nämlich:
"Browser, ich schicke Dir zwar jetzt gleich eine Seite. Aber da ich nicht weiß, ob die Seite gehackt worden ist: Führe mögliches Inline-JavaScript nicht aus".
Allerdings erzeugt das einen mächtigen Schutz gegenüber Angriffstechniken, bei denen Nutzer bzw. Hacker versuchen, JavaScript in die Seiten einzuschmuggeln. Sei es durch Sql-Injektionen oder durch XSS-Hacks. Spätestens seit dem Aufkommen von Ransomware (Verschlüsselungssoftware), die über gehackte Websites verteilt wird, wird so ein Schutzkonzept relevant.
Wer mehr Infos sucht:
Content Security Policy
https://de.wikipedia.org/wiki/Content_Security_Policy
Bei w3.org: CSP
https://www.w3.org/TR/CSP/#status
.
Aber: Das Konzept ist relativ neu. Vor 13 - 16 Jahren, als ich Server-Daten entwickelt hatte, gab es das noch nicht. Umgekehrt waren damals viele Dinge noch so wenig ausgereift, daß es zu einem
<button onclick="funktionsname(Parameterwert)">Absenden</button>
nicht so wirklich eine Alternative gab. Sprich: Viele nicht ganz neue Webdienstleistungen verwenden Inline-JavaScript. So daß man fragen kann: Lohnt sich der Einbau einer CSP? Dieselbe Frage stellt sich bsp. für jedes Content Management System (WordPress, Joomla usw.).
Zwar kann man ein System grundsätzlich gegen Sql-Injektionen schützen, indem alle Zugriffe vom Webserver auf den Datenbank-Server ausschließlich über gespeicherte Prozeduren ausgeführt werden, die keinen Sql-Code dynamisch zur Ausführungszeit zusammenbauen. Dann sind Sql-Injektionen per definitionem unmöglich.
Ferner kann man ein System grundsätzlich gegen XSS-Hacks schützen, indem alle Ausgabeseiten über eine Xsl-Transformation laufen. Denn damit werden Ausdrücke der Form <script> automatisch als <script> ausgegeben und sind damit harmlos.
Beide Techniken werden innerhalb von Server-Daten von Anfang an genutzt. Damit gibt es über direkte Nutzereingaben eigentlich keine Lücken, um JavaScript einzuschleusen.
Allerdings: Es gibt die "internen blauen Seiten". Darüber können Ausgabeseiten / Html-Seiten erstellt werden. Diese können zusätzliches JavaScript enthalten. Analog gibt es bei vielen Content Management Systemen die Möglichkeit, rohen Html-Code und JavaScript-Code in die Seite einzubauen. Was also wäre der Worst Case bei einem gehackten Administratoraccount? Daß über diesen Weg kritischer JavaScript-Code verteilt wird.
An dem Punkt reifte bei mir die Einsicht: Eine Content Security Policy, die Inline-JavaScript strikt unterbindet, wäre ein Schutz in so einer Situation.
Aber: Wie läßt sich eine bestehende Anwendung "verträglich" umbauen? Pro Seite eine externe JavaScript-Datei anzulegen wäre aussichtslos. Ferner muß es natürlich möglich sein, die Standardtechniken zu nutzen, ohne manuell eine externe JavaScript-Datei erstellen zu müssen.
Nach diversem Herumbauen entwickelte sich im letzten Sommer eine Lösung:
(1) Die bestehenden Inline-JavaScripts sind meist von der Form onclick="Funktionsname(Parameter1, Parameter2); return false;".
(2) Daraus wurden zwei Attribute: sd:type="Funktionsname" sd:value="Parameter1, Parameter2"
(3) Da alle Ausgabeseiten über eine zentrale Xsl-Transformation laufen, konnten alle internen JavaScript-Definitionen umgeschrieben werden. Gibt es in einer Ausgabeseite einen Ausdruck der Form <button onclick="Funktionsname('Parameter1', 'Parameter2')">, ließ sich dieser in der zentralen Xsl-Transformation ebenfalls auf das Schema sd:type/sd:value umschreiben.
(4) Die zentrale JavaScript-Datei, die in allen Ausgabeseiten eingebunden ist, sucht nach allen Elementen mit diesen beiden Attributen. Die Liste der Parameter (reiner String) wird am Komma gesplittet, die Arraywerte werden einzelnen Parametern zugewiesen. Mit all diesen Daten wird (Übergabe per Wert) eine Funktion aufgerufen.
(5) Diese prüft, ob der Funktionsname in einer Liste bekannter Funktionsnamen drin ist und setzt in diesem Fall den Eventhandler für das Element. Da kann der Funktionsname direkt verwendet und mit der richtigen Zahl der Parameter (die nun alle String-Parameter sind) definiert werden.
(6) Folgerung: Bei manchen Funktionen mußte die Definition geändert werden. Etwa wenn als Parameter ein Objekt per document.getElementById("id") übergeben wurde. Stattdessen wird der String "id" direkt übergeben, die Funktion sucht nach dem passenden Element. Analog bei der Übergabe von String-Arrays: Da genügt es, die Einzelstrings mit einem Zeichen wie | zu verketten und einen einzigen langen String zu übergeben. Der von der Funktion gesplittet wird.
(7) Die Wirkung: Es wird nur für jene Funktionsnamen ein Eventhandler definiert, die bekannt sind. Was diese Funktionen machen, steht in der externen Datei und läßt sich deutlich schwieriger hacken.
(8) Die Konsequenz: Diese Technik unterbindet eingeschleustes Inline-JavaScript sogar dann, wenn der Browser offiziell Content Security Policy noch gar nicht unterstützt. Unbekannte Funktionsnamen erzeugen keine Eventhandler, damit wird nichts ausgeführt.
Die erste Lösung war noch "ziemlich unbrauchbar". Die zweite Lösung arbeitete noch mit eval, um die Eventhandler zu definieren. Aber die Freigabe von eval unterläuft den Schutz. Beim dritten Durchgang ergab sich diese Lösung. Es war zwar etwas aufwendig, alle Funktionen der Reihe nach so zu überprüfen. Aber da damit alles an Inline-JavaScript-Eventhandlern verschwindet, kann der Browser die externe JavaScript-Datei einmal kompilieren und führt sie doch sehr schnell aus.
Das Gesamtsystem konnte auch nicht "in einem Rutsch" umgestellt werden. Stattdessen verzweigte der .NET-Code, so daß zunächst auf dem Testsystem (interner Port 442, Kopie der .NET-Anwendung, JavaScript- und Xsl-Datei) die neue Xsl-Datei geladen wurde, die ihrerseits die neue JavaScript-Datei einbindet. Alle Datenbanken auf dem Testsystem laufen damit auf der neuen Logik und können getestet werden. Dann gab es ein Flag pro Datenbank, das CSP für diese Datenbank auf dem Hauptsystem aktivierte. Mit dieser Logik wurden die ersten - kleinen - Datenbanken im Juni 2017 umgestellt. Schritt für Schritt folgten weitere, unterbrochen von anderen Arbeiten. Inzwischen ist das fast fertig. Neue Datenbanken laufen sofort auf CSP. Die letzten älteren sind grade in der Prüfung.
Die in manchen Fällen gewünschte Einbindung externer Bibliotheken (etwa Google) ließ sich analog absichern. Der CSP-Header wird zusätzlich der Xsl-Transformation übergeben. Diese prüft bei script-Elementen, ob der src-Wert lokal oder im CSP-Header enthalten ist. Falls nein, wird das Script in der Form
src="/-cspr.html?f=externeAdresse"
eingebaut. Das führt dazu, daß die Monitoring-Seite /-cspr.html über diesen Versuch informiert wird.
Bei einigen Bibliotheken war ein Umstieg auf eine neuere, CSP-verträgliche Variante notwendig. Eine Bibliothek wie jquery.mobile-1.4.5.min.js definierte hartnäckig ein onfocus-Attribut. Da war schließlich eine andere Lösung für etwas wie swipeleft / swiperight notwendig. Aber all dies ließ sich mit vertretbarem Aufwand erledigen.
Fazit: Die damalige Architekturentscheidung, alle Ausgabeseiten über eine zentrale Xsl-Transformation abzuwickeln, ermöglichte es nun, mit einem vertretbaren Aufwand eine Content Security Policy innerhalb von Server-Daten zu implementieren. Die nun sogar Nutzer mit "zu alten Browsern" schützt.
