Strict Transport Security in ASP.NET MVC: Implementing RequireHstsAttribute
Learn how to better protect your web applications using a simple, and easy-to-use ASP.NET Core attribute, RequireHsts, which requires your browser to use HTTPS.
Join the DZone community and get the full member experience.
Join For FreeHTTPS is the core mechanism for accessing web resources in a secure way. One of the limitations of HTTPS is the fact that the user can manually provide a URL which doesn't contain the proper schema. In most cases, this will result in the application sending a redirect response which will tell the browser to re-request the resource using HTTPS. Unfortunately, this redirect creates a risk of a Man-in-the-Middle attack. Strict Transport Security is a security enhancement which allows web applications to inform browsers that they should always use HTTPS when accessing a given domain.
Strict Transport Security defines Strict-Transport-Security header with two directives: required max-age and optional includeSubDomains. From the moment the browser receives the Strict-Transport-Security header, it should consider the host as a Known HSTS Host for the number of seconds specified in the max-age directive. Being a Known HSTS Host means that the browser should always use HTTPS for communication. In the initially described scenario (user providing HTTP schema or no schema at all), the browser should cancel the initial request by itself and change the schema to HTTPS. Specifying the includeSubDomains directive means that a given rule applies also to all subdomains of the current domain.
In order to implement this behavior in an ASP.NET MVC application, we need to fulfill two requirements: issue a redirect when a request is being made with HTTP, and send the header when a request is being made with HTTPS. The first behavior is already available through RequireHttpsAttribute so we can inherit it - we just need to add the second.
public class RequireHstsAttribute : RequireHttpsAttribute
{
private readonly uint _maxAge;
public uint MaxAge { get { return _maxAge; } }
public bool IncludeSubDomains { get; set; }
public RequireHstsAttribute(uint maxAge)
: base()
{
_maxAge = maxAge;
IncludeSubDomains = false;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (filterContext.HttpContext.Request.IsSecureConnection)
{
StringBuilder headerBuilder = new StringBuilder();
headerBuilder.AppendFormat("max-age={0}", _maxAge);
if (IncludeSubDomains)
{
headerBuilder.Append("; includeSubDomains");
}
filterContext.HttpContext.Response.AppendHeader("Strict-Transport-Security", headerBuilder.ToString());
}
else
{
HandleNonHttpsRequest(filterContext);
}
}
}
We can now use this attribute, for example, by adding it to our global filters collection.
protected void Application_Start()
{
...
GlobalFilters.Filters.Add(new RequireHstsAttribute(31536000) { IncludeSubDomains = true, Preload = true });
}
From this moment, our application will be "enforcing" HSTS. But the initial problem still has not been fully resolved - there is still that one redirect which can happen if the application is not accessed over HTTPS the first time. This is why HSTS Preload List has been created. This service allows for submitting domains which should be hardcoded, as Known HSTS Hosts, in the browsers - this removes the risk of that one potential redirect. The service is hosted by Google, but all major browsers vendors have stated that they will be using the submitted list of domains.
If one wants to include his/her application on the HSTS Preload List, after submitting the domain additional steps needs to be taken. The application must confirm the submission by including preload directive in the Strict-Transport-Security header and fulfill some additional criteria:
- Be HTTPS only and serve all subdomains over HTTPS.
- The value of max-age directive must be at least eighteen weeks.
- The includeSubdomains directive must be present.
Some small adjustments to our attribute are needed in order to handle this additional scenario.
public class RequireHstsAttribute : RequireHttpsAttribute
{
...
public bool Preload { get; set; }
public RequireHstsAttribute(uint maxAge)
: base()
{
...
Preload = false;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
...
if (filterContext.HttpContext.Request.IsSecureConnection)
{
if (Preload && (MaxAge < 10886400))
{
throw new InvalidOperationException("In order to confirm HSTS preload list subscription expiry must be at least eighteen weeks (10886400 seconds).");
}
if (Preload && !IncludeSubDomains)
{
throw new InvalidOperationException("In order to confirm HSTS preload list subscription subdomains must be included.");
}
...
if (Preload)
{
headerBuilder.Append("; preload");
}
filterContext.HttpContext.Response.AppendHeader("Strict-Transport-Security", headerBuilder.ToString());
}
else
{
HandleNonHttpsRequest(filterContext);
}
}
}
Now we have full HSTS support with preloading in an easy to use attribute just waiting to be used in your application. You can find cleaned up source code here.
Published at DZone with permission of Tomasz Pęczek. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments