This post is a sum-up of how to make it more difficult for hackers to compromise your ASP.NET (MVC, Web API, Core) website in general. In the following paragraphs, I have listed a number of HTTP headers that are easily configured and that everyone should implement.
Security is not an afterthought and you should think about how to secure your applications from the start. There are many levels of security from authentication and authorization, to configuring web servers and firewalls, SSL certificates and encryption, etc. In this post, I want to show you how you can configure HTTP headers in your ASP.NET Core applications to improve security. These are standard headers that need to be implemented in all websites and web applications, regardless of the framework of use and how you've built your application.
If you're not already familiar with SecurityHeaders.com, check it out and add it to your toolbox. Created by Scott Helme, this handy tool scans your website and makes suggestions as to which HTTP response headers to add in order to improve security.
Table of Contents
Let's start by discussing how to modify headers in ASP.NET Core. Like ASP.NET (MVC) there are multiple ways of modifying headers. This post introduces two different ways:
- Through middleware
- In
web.config
Headers in middleware
This is my favorite approach, easy to use and elegant. Specifying headers in middleware can be done in C# code by creating one or more pieces of middleware. Most examples in this post will use this approach. In short, you either create a middleware class or call the Use
method directly in the Configure
method in Startup.cs
:
app.Use(async (context, next) =>
{
context.Response.Headers.Add("Header-Name", "Header-Value");
await next();
};
The code above adds a header called Header-Name
to all responses. It's important to call the Use
method before calling UseEndpoints
, UseMvc
, and similar configurations.
A quick word about adding headers in middleware while also using the UseStatusCodePagesWithReExecute
method. UseStatusCodePagesWithReExecute
executes the configured error page within the same HTTP context as the original request. This means that your header adding middleware is executed twice. In this setup, make sure to check the Headers
collection to avoid duplicates and exceptions:
if (!context.Response.Headers.ContainsKey("Header-Name"))
{
context.Response.Headers.Add("Header-Name", "Header-Value");
}
Headers in web.config
ASP.NET Core no longer needs a web.config
file, and application settings have moved to a JSON model. But since most people host their ASP.NET Core websites on IIS, a web.config
file is still perfectly valid. Plus, if you're hosting on IIS, you'd probably need a web.config
file anyway to configure IIS-related settings. While the system.web
, appsettings
, connectionStrings
, and other root elements no longer apply, the system.webServer
element is for the web servers' eyes only.
Custom headers in ASP.NET Core can be added by adding a web.config
file to the root of the website directory and including the following code:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Header-Name" value="Header-Value" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
This configuration does exactly the same as we saw with the middleware code. This adds a header named Header-Name
with the value of Header-Value
. In some cases, you will need to use the web.config
file to remove headers, I'll guide you through that in a moment.
Headers for improved security
I'll introduce you to a number of HTTP headers and how to configure them in ASP.NET Core, but make sure to read more about them to understand each header better.
X-Frame-Options
Hackers use iframes to trick your users into clicking unintended links. The X-Frame-Options
header tells clients (web browsers) that framing isn't allowed. The header can be easily added using middleware:
context.Response.Headers.Add("X-Frame-Options", "DENY");
You can change the value to SAMEORIGIN
to allow your website to iframe pages.
Blog posts throughout the web mention that the X-Frame-Options
header is automatically added with the SAMEORIGIN
value when enabling anti-forgery:
services.AddAntiforgery();
Either I'm doing something wrong or that feature was removed in recent ASP.NET Core releases. I don't see the header automatically being added. In any case, if you want full control of the header, make sure to disable the automatic feature:
services.AddAntiforgery(options =>
{
options.SuppressXFrameOptionsHeader = true;
});
X-XSS-Protection
The X-XSS-Protection
header will cause most modern browsers to stop loading the page when a cross-site scripting attack is identified. The header can be added through middleware:
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
The value 1
means enabled and the block
mode will block the browser from rendering the page.
X-Content-Type-Options
MIME-type sniffing is an attack where a hacker tries to exploit missing metadata on served files. The header can be through middleware:
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
The value nosniff
prevents primarily old browsers from MIME-sniffing.
If you want to cover static files as well, the header can be added to the web.config
file instead:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="X-Content-Type-Options" value="nosniff" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
Referrer-Policy
When you click a link on a website, the calling URL is automatically transferred to the linked site. Unless this is necessary, you should disable it using the Referrer-Policy
header:
context.Response.Headers.Add("Referrer-Policy", "no-referrer");
There are a lot of possible values for this header, like same-origin
that will set the referrer as long as the user stays on the same website.
X-Permitted-Cross-Domain-Policies
You're probably not using Flash, right? Right!? If not, you can disable the possibility of Flash making cross-site requests using the X-Permitted-Cross-Domain-Policies
header:
context.Response.Headers.Add("X-Permitted-Cross-Domain-Policies", "none");
Strict-Transport-Security
All pages (and responses) should be served over HTTPS. To make sure that none of your content is served over HTTP, set the Strict-Transport-Security header. The header can be set using custom middleware like in the previous examples, but ASP.NET Core has a built-in middleware for this named HSTS (HTTP Strict Transport Security Protocol):
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
// ...
}
else
{
app.UseHsts();
}
}
As shown in the code above, it is recommended to use HSTS in production only, to avoid possible issues when developing locally. You can customize the Strict-Transport-Security
header's values too:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddHsts(options =>
{
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(365);
});
}
The cove above will produce a header with subdomains included and a max-age of 1 year:
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Powered-By
Like ASP.NET, ASP.NET Core returns the X-Powered-By
header when you host your website on IIS. This means that you cannot remove the header in middleware, since this is managed outside of ASP.NET Core. web.config
to the rescue:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
Server
Like X-Powered-By
, IIS kindly identifies itself in the Server
header. While hackers could probably quickly find out anyway, you should still make it harder for them by removing this header. There's a dedicated security feature available in web.config
for doing that:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<security>
<requestFiltering removeServerHeader="true" />
</security>
</system.webServer>
</configuration>
Permissions-Policy
The Permissions-Policy
header (formerly known as Feature-Policy
) tells the browser which platform features your website needs. Most web applications won't need to access the microphone, or the vibrator functions available on mobile browsers. Why not be explicit about it to avoid imported scripts or framed pages, doing things you don't expect?
context.Response.Headers.Add("Permissions-Policy", "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
There have been big changes since this was called Feature-Policy
, make sure to have a read.
Content-Security-Policy
The Content-Security-Policy header (CSP) needs a long post for itself, but basically, this header helps to prevent code injection attacks like cross-site scripting and clickjacking, by telling the browser which dynamic resources are allowed to load. You can add this header easily via middleware:
context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'");
Apart from how you configure this header, you'd need to make sure you've added the domains that your application relies on too. For instance, if your website loads scripts and stylesheets from cdnjs, you need to correctly add the domain to your CSP, otherwise, the resources will be blocked by the browser.
ASP.NET Core Middleware Guide
Creating custom middleware couldn't be easier. Say you have an ASP.NET Core web application, create a folder named Middleware
and create a class named PoweredByMiddleware.cs
:
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace Pegasus.WebUI.Middleware
{
public class PoweredByMiddleware
{
private readonly RequestDelegate _next;
public PoweredByMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext httpContext)
{
httpContext.Response.Headers["X-Powered-By"] = "Pegasus";
return _next.Invoke(httpContext);
}
}
}
This piece of middleware adds the X-Powered-By
header with the value of Pegasus
. Now how do we use it? Open your Startup.cs
file and add the following statement to the Configure
method:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiddleware<PoweredByMiddleware>();
// ...
}
Brilliant, that's all there is to it. Let's create a middleware that encapsulates the security headers we just discussed. Create a class named SecurityHeadersMiddleware.cs
with the following code:
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace Pegasus.WebUI.Middleware
{
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
public SecurityHeadersMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext httpContext)
{
// X-Frame-Options
if (!httpContext.Response.Headers.ContainsKey("X-Frame-Options"))
{
httpContext.Response.Headers.Add("X-Frame-Options", "DENY");
}
// X-Xss-Protection
if (!httpContext.Response.Headers.ContainsKey("X-XSS-Protection"))
{
httpContext.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
}
// X-Content-Type-Options
if (!httpContext.Response.Headers.ContainsKey("X-Content-Type-Options"))
{
httpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff");
}
// Referrer-Policy
if (!httpContext.Response.Headers.ContainsKey("Referrer-Policy"))
{
httpContext.Response.Headers.Add("Referrer-Policy", "no-referrer");
}
// X-Permitted-Cross-Domain-Policies
if (!httpContext.Response.Headers.ContainsKey("X-Permitted-Cross-Domain-Policies"))
{
httpContext.Response.Headers.Add("X-Permitted-Cross-Domain-Policies", "none");
}
// Permissions-Policy
if (!httpContext.Response.Headers.ContainsKey("Permissions-Policy"))
{
httpContext.Response.Headers.Add("Permissions-Policy", "accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'");
}
// Content-Security-Policy
if (!httpContext.Response.Headers.ContainsKey("Content-Security-Policy"))
{
httpContext.Response.Headers.Add("Content-Security-Policy", "form-action 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com https://code.jquery.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net");
}
return _next.Invoke(httpContext);
}
}
}
Before adding each header, the code checks whether the header exists in the Headers
collection or not, this is to prevent duplicates and runtime exceptions. The Content-Security-Policy
header shows a basic example, but you'd need to modify the domains that your application makes use of. You use the middleware just as you saw in the previous example:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiddleware<SecurityHeadersMiddleware>();
// ...
}
Lastly, here's a web.config
file showing how you can remove the X-Powered-By
and Server
headers:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<security>
<requestFiltering removeServerHeader="true" />
</security>
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>