Web Security für alle: HSTS
Published on Thursday, September 22, 2016 4:00:00 AM UTC in Programming & Security
Neben der eigentlichen Funktionalität für den Redirect zu https aus dem letzten Post gibt es eine ganze Reihe sicherheitsrelevanter Header, die man für seine Webseite umsetzen muss sollte kann. Die konkreten Maßnahmen hängen ganz von den persönlichen Ansprüchen, der Rolle der eigenen Seite und natürlich den verfügbaren Ressourcen und Fähigkeiten ab. Mozilla gibt in seinen Guidlines ein Cheat Sheet vor, das nicht nur eine sinnvolle Reihenfolge für die Umsetzung von Maßnahmen vorzugeben versucht, sondern auch den zu erwartenden Benefit aufführt. Ich beginne bei der Umsetzung mit dem http strict transport security-Header, oder kurz und einfach von der Zuge gehend: HSTS.
Eine Middleware für Header
Um die Header an zentraler Stelle und kontrolliert umsetzen zu können, erstelle ich wieder eine Middleware als gemeinsames Grundgerüst für alle folgenden Implementierungen.
Um Header zu manipulieren, könnte man ganz naiv selbst einen vermeintlich guten Zeitpunkt wählen, um an dem Response-Objekt, das man in der Middleware im Zugriff hat, herumzuschrauben. Tatsächlich ist es aber so, dass das Senden von Headern nicht mehr möglich ist, wenn die Übermittlung des Body einmal begonnen hat. Daher ist es gar nicht so einfach, diesen Zeitpunkt selbst zu bestimmen. Glücklicherweise gibt es aber einen besseren Mechanismus: man registriert einfach ein Callback, das genau in dem Augenblick aufgerufen wird, bevor die Header zum Client geschickt werden. Dann kann man seine eigene Logik ausführen und sichergehen, dass die Header auch wirklich gesetzt sind bzw. beim Client landen. Etwa so:
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
public SecurityHeadersMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
context.Response.OnStarting(ApplyHeaders, context);
await _next(context);
}
private Task ApplyHeaders(object state)
{
var context = (HttpContext)state;
// add important header
context.Response.Headers["x-somethingsomething"] = "dark side";
return Task.CompletedTask;
}
}
Dieses Pattern ist auf der offiziellen Seite dokumentiert und kann auch für andere Zwecke genutzt werden, etwa wenn es um die Manipulation von Cookies geht.
Für die Umsetzung der einzelnen relevanten Header könnte man nun einen hochgradig generischen Ansatz wählen, der elegant und flexibel konfigurierbar ist. Da ich aber keine wiederverwendbare Komponente für die allgemeine Nutzung bauen möchte, wird sich meine Implementierung auf fest verdrahtete Aufrufe zur Ausgestaltung der einzelnen nötigen Header konzentrieren. Lediglich einzelne Aspekte möchte ich gerne in die Konfiguration auslagern.
Dazu erstelle ich eine Konfigurationsklasse, die ich wie bei der Redirect-Middleware aus dem letzten Artikel in der Startup-Klasse zunächst aus der Konfiguration befülle und dann per Dependency Injection in die Middleware hineinreiche.
// First some configuration options...
public class SecurityHeadersOptions
{
public HstsOptions Hsts { get; set; }
public class HstsOptions
{
public bool IsEnabled { get; set; }
public int MaxAge { get; set; } // in seconds
public bool IncludeSubDomains { get; set; }
public bool Preload { get; set; }
}
}
// ... bound to the configuration in the "ConfigureServices"-method of the Startup class...
services.Configure<SecurityHeadersOptions>(options => Configuration.GetSection("SecurityHeadersOptions").Bind(options));
// ... then injected into the middleware:
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
private readonly SecurityHeadersOptions _options;
public SecurityHeadersMiddleware(RequestDelegate next, IOptions<SecurityHeadersOptions> options)
{
_next = next;
_options = options.Value;
}
// ...
}
Die zugehörige Konfiguration kann dann so aussehen (appsettings.json):
"SecurityHeadersOptions": {
"Hsts": {
"IsEnabled": true,
"MaxAge": 31536000,
"IncludeSubDomains": false,
"Preload": false
}
}
Und schließlich das Hinzufügen des eigentlichen Headers, falls gewünscht und konfiguriert, implementiert in der Middleware:
private Task ApplyHeaders(object state)
{
var context = (HttpContext)state;
ApplyHstsHeader(context);
return Task.CompletedTask;
}
private void ApplyHstsHeader(HttpContext context)
{
var hstsOptions = _options.Hsts;
if (!context.Request.IsHttps || !hstsOptions.IsEnabled)
{
return;
}
var maxAgePart = $"max-age={hstsOptions.MaxAge}";
var includeSubDomainsPart = hstsOptions.IncludeSubDomains ? "; includeSubDomains" : string.Empty;
var preloadPart = hstsOptions.Preload ? "; preload" : string.Empty;
var header = $"{maxAgePart}{includeSubDomainsPart}{preloadPart}";
context.Response.Headers["Strict-Transport-Security"] = header;
}
Was das nun alles bedeutet, ist im folgenden kurz erklärt.
HSTS
Der strict transport security-Header ist sowas wie der https-Redirect auf Crack. Denn anstatt des Servers führt der Browser des Benutzers diese "Umleitung" aus, wenn er durch einen solchen Header dazu angewiesen wird. Sprich: egal ob der Benutzer explizit eine http-Verbindung anfordert, der Browser macht automatisch eine Anfrage per https daraus. Der geneigte Leser hat natürlich schon erkannt, dass hier ein klassisches Henne-Ei-Problem vorliegt: der Header ist ja Teil einer Server-Antwort, möchte aber den Browser zu einem bestimmten Verhalten bei der Server-Anfrage zwingen. Da scheint die Logik Kopf zu stehen(?). Deshalb gibt es die Angabe max-age
: der Browser merkt sich das Vorhandensein des Headers, sobald er ihn das erste Mal antrifft, und führt dann für die angegebene Zeitspanne künftige Anfragen an den Server nur noch per https aus.
Um Manipulationen etwa durch Man in the middle-Angriffe auszuschließen, darf der Header nur bei einer korrekten, gesicherten Verbindung ausgeliefert bzw. vom Browser berücksichtigt werden. Bei einer Verbindung per http sollte man den Header nicht ausliefern (vgl. auch den Code oben).
Die verschärfte Variante des Konstrukts wird durch den optionalen Parameter preload
gesteuert: Google pflegt für Chrome eine Liste von Seiten, die HSTS nutzen (auch andere Browser bedienen sich aus dieser Liste). Beim Ansteuern einer dort aufgeführten Seite wird auch der erste Request direkt per https ausgeführt. D.h. dass beim korrekten Nutzen der preload
-Funktionalität der Benutzer idealerweise gar nie eine ungesicherte Verbindung zum Server aufbaut, auch nicht um den HSTS-Header zum ersten Mal abzuholen.
IncludeSubDomains
, wie unschwer am Namen zu erkennen, gibt als weiterer optionaler Parameter an, ob der Header für alle Sub-Domains gelten soll.
HSTS ist einer der Header, die in der Praxis und bei Fehlkonfiguration zu echten Problemen führen können. Die geforderten Minimalwerte für max-age
sind sehr hoch (18 Wochen bei preload
bzw. 6 Monate bei Mozilla) und bedeuten in der Konsequenz, dass der Browser des Benutzers ein halbes Jahr lang die Verbindung per http zu einem Server verweigert. Entschließt man sich dazu, https zu einem späteren Zeitpunkt wieder abzuschalten, sperrt man dadurch Benutzer effektiv vom Zugriff auf die eigenen Seiten aus. Firefox kennt eine Möglichkeit des Zurücksetzens des Headers, bei Chrome muss man ein Entfernen etwa aus der Preload-Liste manuell beantragen, was wohl langwierig sein kann und begründet werden muss(!). Man sollte sich also im Klaren sein, dass das Nutzen des Headers gleichzusetzen ist mit der Entscheidung, langfristig und dauerhaft https zu unterstützen. Ich empfehle daher dringend die Lektüre der Ausführungen bei Mozilla und auch bei der HSTS-Preload-Liste, bevor man eine entsprechende Implementierung aktiviert.
Die oben gezeigte Implementierung setzt diesen Header je nach Konfiguration um.
Und was bringt's?
Nun, neben dem wohlig-weichen Gefühl in der Magengegend erreicht man mit dieser Verbesserung auch die atemberaubende Steigerung der Note in Mozillas Observatory zu...
... einem D+! Na immerhin! :)
Tags: ASP.NET Core · HSTS · Https