Strict Transport Security in ASP.NET MVC - Implementing RequireHstsAttribute

HTTPS is the core mechanism for accessing web resources in secure way. One of the limitations of HTTPS is the fact that user can manually provide an URL which doesn't contain the proper schema. In most cases this will result in application sending redirect response which will tell the browser to re-request the resource using HTTPS. Unfortunately this redirect creates a risk of 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 given domain.

Strict Transport Security defines Strict-Transport-Security header with two directives: required max-age and optional includeSubDomains. From the moment browser receives the Strict-Transport-Security header it should consider the host as a Known HSTS Host for the number of seconds specified in max-age directive. Being a Known HSTS Host means, that browser should always use HTTPS for communication. In the initially described scenario (user providing HTTP schema or no schema at all) browser should cancel the initial request by itself and change the schema to HTTPS. Specifying includeSubDomains directive means that given rule applies also to all subdomains of current domain.

In order to implement this behavior in ASP.NET MVC application we need to fulfill two requirements: issue a redirect when request is being done with HTTP and send the header when request is being done 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 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 accessed for the first time not over HTTPS. 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 included his 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 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 easy to use form of attribute - just waiting to be used in your application. You can find cleaned up source code here.