Windows ist dafür nicht sicher genug Web Security für alle: X-XSS-Protection

Web Security für alle: Content-Security-Policy

Published on Wednesday, September 28, 2016 4:00:00 AM UTC in Programming & Security

Der Content-Security-Policy-Header ist so ein bisschen der Schweizer-Armee-Header der Sicherheitseinstellungen. Er kann sehr fein granuliert steuern, was der eigenen Seite erlaubt sein soll: wohin man sich verbinden darf, woher Skripte, Styles und Schriftarten geladen werden, was an Inhalten eingebettet werden kann und wo die eigenen Inhalte eingebettet werden dürfen, von wo Bilder, Objekte und andere Inhalte stammen müssen. Damit verhindert er - zumindest mit den strengsten Einstellungen - so gut wie alle XSS-Angriffe. In gewisser Weise löst dieser Header auch die bereits vorgestellten Funktionen von X-Frame-Options und X-XSS-Protection ab, weil man mit ihm dasselbe und noch mehr erreichen kann. Warum ich das erst jetzt erzähle? Nun, der Content-Security-Policy-Header ist relativ neu und gut für moderne Browser geeignet, während die eben genannten Header zwar im direkten Vergleich weniger bieten, dafür aber auch mit älteren Browsern funktionieren. Zumindest momentan haben also noch alle ihre Daseinsberechtigung.

What's in the box

Prinzipiell folgt der Aufbau der einzelnen Bestandteile eines Content-Security-Policy-Headers dem folgenden Schema:

<resource> <source-1> <source-2> <source-n>;

Man gibt also zunächst die Ressource an, um die es geht, gefolgt von allen Quellen, die man zulassen möchte. Quellenangaben können sein:

  • Schema-Angaben (z.B.: https:)
  • Host-Namen (z.B.: example.com)
  • URIs (z.B.: https://example.com)
  • URIs mit bestimmten Wildcards (z.B.: https://*.example.com)
  • Keywords
    • 'none' (kein Match)
    • 'self' (selber Origin)
    • 'unsafe-inline' (Inline-Ressourcen)
    • 'unsafe-eval' (JavaScript-Funktionen zum dynamischen Ausführen von Code)

Etwas konkreter: wenn ich beispielsweise zulassen möchte, dass mein eigenes JavaScript und jenes von jQuerys CDN zugelassen sein soll, sähe der Eintrag so aus:

script-src 'self' https://code.jquery.com;

Man beachte, dass 'self' in Hochkomma steht, während die konkrete Uri ohne angegeben ist. Abgeschlossen wird der Eintrag per Semikolon.

Wie hilft das nun gegen XSS? Nun, wenn ein Angreifer es schafft, auf irgendeinem Weg ein Skript von evil-attacker.com einzubinden, wird dieses vom Browser nicht ausgeführt, da es nicht von der Liste der erlaubten Quellen stammt. Ebenso werden reflective XSS-Attacken u.ä. verhindert, weil der Ressourcen-Eintrag es nicht erlaubt, Scripte inline auszuführen.

Der Nachteil (oder Vorteil, wie auch immer man es auslegen mag) für die eigenen Programmiergewohnheiten ist, dass man sehr diszipliniert vorgehen muss: hat man etwa (wie empfohlen) die Option zum Erlauben von Inline-JavaScript nicht aktiviert, muss jeder auch noch so kleine Schnipsel Skript-Code über ein Script-Tag (mit src-Angabe!) geladen werden. Genauso verhält es sich mit Styles, die man bei strenger Konfiguration auch nicht mehr inline per style-Attribut einbauen darf. Man erzieht sich also nebenbei zu einem sehr disziplinierten Entwickeln.

Verstöße gegen die Vorgaben kann man zur Entwicklungszeit einfach in der Konsole des Browsers erkennen:

csp-errors-browser-console.png

Es gibt aber auch die Möglichkeit (ähnlich wie beim X-XSS-Protection-Header), Verstöße per POST an eine Adresse schicken zu lassen, so dass man diese auch später zur Laufzeit einfach bemerkt und reparieren kann.

Neben den konkreten Ressourcen gibt es noch die allgemeine Ressource default-src, die als Fallback verwendet wird für alles mögliche, was man nicht explizit konfiguriert hat. Das erlaubt es, auf einen Schlag eine mehr oder weniger globale Konfiguration zu erstellen, ohne mühsam einzeln Regeln für Skripte, Styles, Bilder, Schriftarten und alles mögliche andere aufzustellen.

Selbstverständlich gibt es auch bei diesem Header wieder Sonderfälle und Ausnahmen, etwa um per Nonce oder Hash doch noch einzelne Inline-Skripte u.ä. zuzulassen, wenn es darum geht, die Aufwände zur Migration bestehender Seiten zunächst gering zu halten. Das alles ist sehr ausführlich beispielsweise bei HTML5 Rocks dokumentiert.

Für eine kleinere Seite wie diese hier könnte ein Header so aussehen:

default-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com;

Erläuterung:

  • Per Standard-Einstellung (default-src) wird zunächst mal festgelegt, dass alles, was nicht explizit konfiguriert ist, nur denselben Origin (also die eigenen Seite) als Quelle zulässt.
  • Styles aber werden zusätzlich von https://fonts.googleapis.com erlaubt; das ist nötig, weil von dort CSS-Angaben zu den verwendeten Schriftarten von Google Fonts geladen werden.
  • Die Schriftarten selbst kommen von einem anderen Host, nämlich https://fonts.gstatic.com, weshalb auch dieser erlaubt werden muss. Da ich z.B. Font-Awesome lokal hoste, ist aber auch hier 'self' explizit eingetragen.

Die Überarbeitung der eigenen Seite, bis alles mit möglichst restriktiven Angaben tatsächlich funktioniert, kann je nach Umfang des Codes und vorhandener Altlasten wirklich aufwändig und langwierig werden und sollte ggfs. gut geplant und ausgiebig getestet werden.

Implementierung

Da es sehr viele Spielarten und Kombinationsmöglichkeiten für den Header gibt, habe ich mich darauf beschränkt, einen wörtlichen Eintrag aus der Konfiguration einfach unverändert durchzureichen. Das sieht dann auf der Konfigurationsseite in altbekannter Manier etwa so aus:

"ContentSecurityPolicy": {
  "IsEnabled": true,
  "Value": "default-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com;"
}
public ContentSecurityPolicyOptions ContentSecurityPolicy { get; set; }

public class ContentSecurityPolicyOptions
{
    public bool IsEnabled { get; set; }
    public string Value { get; set; }
}

Und wieder die zugehörige, simple Implementierung in der bisher schon genutzten Middleware:

private Task ApplyHeaders(object state)
{
    var context = (HttpContext)state;

    ApplyHstsHeader(context);
    ApplyXFrameOptionsHeader(context);
    ApplyXContentTypeOptionsHeader(context);
    ApplyXXssProtectionHeader(context); 
    ApplyContentSecurityPolicyHeader(context); // <-- look, new and fancy
            
    return Task.CompletedTask;
}

private void ApplyContentSecurityPolicyHeader(HttpContext context)
{
    var contentSecurityPolicyOptions = _options.ContentSecurityPolicy;

    if (!contentSecurityPolicyOptions.IsEnabled)
    {
        return;
    }

    // logging and stuff

    context.Response.Headers["Content-Security-Policy"] = contentSecurityPolicyOptions.Value;
}

Fertig :).

Was es bringt

Mit der Umsetzung des letzten bemängelten Eintrags in der Bewertungsliste von Mozillas Observatory steigt die Endnote sogar auf ein A+, mit der übererfüllten Punktzahl von 105/100:

grade-with-csp-header.png

Tatsächlich könnte man sogar noch wesentlich mehr Punkte erreichen; in seinen FAQs spricht Mozilla momentan davon, dass 130 Punkte die aktuelle Höchstpunktzahl ist. Die Umsetzung noch weiterer Sicherheitsfeatures ist in meinem Fall allerdings überflüssig, da nicht zutreffend:

scores-with-csp-header.png

Cookies verwende ich für nicht-angemeldete Nutzer keine, Skripte stammen alle vom selben Origin, und HPKP ist nur für "maximum risk sites" empfohlen und haben ebenso ein recht hohes Potenzial zur Fehlanwendung:

Due to the risk of knocking yourself off the internet, HPKP must be implemented with extreme care. This includes having backup key pins, testing on a non-production domain, testing with Public-Key-Pins-Report-Only and then finally doing initial testing with a very short-lived max-age directive. Because of the risk of creating a self-denial-of-service and the very low risk of a fraudulent certificate being issued, it is not recommended for the majority websites to implement HPKP.

Da verzichte ich momentan also lieber drauf und geben mich mit meinem A+ zufrieden :).

Tags: ASP.NET Core · XSS