Security headers - loved by security teams and loathed by developers. They tell the consumers of your web application what to expect and what it can do. The question is, how can you ensure that your application has the right headers set?
I find myself getting more involved in headless CMS development these days. My choice of platform is Kentico Kontent, and I've been building static sites using Gatsby. I found that a large number of sites built this way do not have security headers set. For me, part of a go-live checklist needs to ensure that your application keeps you, your customers and your organisation safe. A good step forward here is setting the security headers on your site.
Let's take a quick look at what security headers are and then take a look at how we can set them.
Why we need security headers
We use security headers to inform the browser of the expectations of our application. This covers things like:
- what external data and script sources we intend to use
- how our application can present itself
- what features of the device our application interacts with
These headers help to keep our application, data, and users safe from attacks. Most of the headers in this article address cross-site scripting (XSS). XSS is the term used when injecting harmful code into an application.
A common requirement of any web application project is to engage the services of a 3rd party to perform penetration or ‘pen’ testing on your application. One of the first things that will be tested for is the security headers. This goes hand-in-hand with the commonly seen ‘Top 10’ from OWASP. As a result, there is a dedicated OWASP security headers project that gives a good level of detail.
But why is this important if you're generating a static site? It depends on what your site (or application) does. As you add external services for customer reviews, contact forms, and eCommerce integration etc., we increase the number of possible vulnerabilities of the application. It may be true that your core data is on accessed when you rebuild your application, but all of those other features added can leave you, your customers, and your organisation exposed. Being frank, even if you don't add external services there is a risk. This risk is easily reduced using some basic security headers.
Which headers should you include?
Typically you want to cover off as many of these as possible. There is however no value in setting a security header in a completely insecure manner. In those instances, I would say that the header can be and probably should be excluded.
The OWASP list of security headers is as follows:
- HTTP Strict Transport Security (HSTS)
- Public Key Pinning Extension for HTTP (HPKP)
- X-Frame-Options
- X-XSS-Protection
- X-Content-Type-Options
- Content-Security-Policy
- X-Permitted-Cross-Domain-Policies
- Referrer-Policy
- Expect-CT
- Feature-Policy
For our purposes, I have chosen a few of the headers that make the most impact concerning XSS. The following three headers are those that get a little more press and are more essential when it comes to application security.
Content-Security-Policy (CSP)
The CSP is one of the stronger ways to protect your application against XSS. In a nutshell, it whitelists sources of approved content for use in your application. The CSP instructs the browser not to load any other content sources in our application.
This is quite a powerful header and is difficult to set up for web applications. It's common for a web application to use many content sources to provide functionality, content, and UI flare. It's tricky because you can set the following directives comprehensively. The list can be found on the Mozilla developer site:
Conveniently, any directive not set, the browser will use the default-src value.
There are a couple of things to note about CSP (which is why it appears more complex):
- Unlike some other headers, you can only set the CSP header once. Whichever is the last one read by the browser is the one which will be used.
- Inline script and styles fall foul of this and require you either set unsafe-inline or perform a bit of additional logic.
Catering for inline scripts and styles needs you to create a hash of the content. It’s mostly a simple thing to do. Chrome makes it quite easy to understand with a helpful message in the console (in this case for <script>alert('Hello, world.')</script>
):
We’re given the has that we need to add as 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng='
. But you can do the following in at the comment line too if you want to:
echo -n "alert('Hello, world.');" | openssl dgst -sha256 -binary | openssl enc -base64
If you're unsure what your CSP needs to contain, you can instead use the Content-Security-Policy-Report-Only
header. For this, you provide your anticipated directives and an endpoint to collect violations. This allows you to build an accurate set of directives before they are applied. This can be especially useful when using 3rd party sources such as Google Tag Manager that may inject content from multiple sources.
For the rest of this article, this is the header I will focus on.
Feature-Policy
Feature Policy allows us to specify which web platform features our application can and cannot use. This caters for vibrate events, camera use and microphone use. The full list of things you can control is extensive and can be found on the Mozilla developer site:
Often referred to as HSTS, this header instructs the user agent to enforce the use of HTTPS, strengthening your implementation of TLS. Under the hood, the purpose of this header is to remind the user agent that this site should be accessed on HTTPS.
The common example is accessing your usual internet banking from a public WiFi location. So long as you’ve been on your bank’s website from a secure location (and HSTS was set), the user agent will know that the site should be accessed over HTTPS. This means that if the public WiFi is compromised in some way, the user agent will not accept the non-HTTPS connection.
As a bonus, because the user agent knows that the application needs to be served over HTTPS, unnecessary 301 redirects for non-HTTPS versions of the application can be avoided.
X-Frame-Options
The X-Frame-Options header instructs the user agent whether you want to allow your site to be framed or not. Preventing the application being embedded in a frame you can help to reduce the likelihood of clickjacking attacks.
How to set the headers
How you build and host your application has a great influence on how you set the security headers. Different hosting providers have different ways to set headers at an application level, and different stacks give you different options too. I.e JAMSTack vs. .NET Core.
Here I’ll touch on how to set these headers in a Gatsby application hosted in Netlify. As an additional bonus, I presented this topic recently at a .NET meetup, so there are some examples for .NET too.
Setting in HTML
One of the simplest methods of setting HTTP response headers for a web page is to add them into the head of the HTML document.
<meta http-equiv="Content-Security-Policy"
content="script-src 'self';
style-src 'self';
image-src ‘self;">
The http-equiv
instructs the user agent to use the supplied content as an HTTP response header. What I've noticed with this in Chrome is that it does not always pick up correctly. You'll still get console notifications if your CSP header is incorrectly set, but it will not necessarily show up in the Network tab when you view the document headers.
This approach also allows you to set individual values for each page, rather than applying the same value across the entire application.
Setting headers in Gatsby.js
When we think about static site generators like Gatsby, we look at separation from the data source and decide that they are secure, as there is no access to the source data. In reality, we add forms and connect other services to create a more rounded application. So for example adding FormStack or Snipcart to our application to add contact forms or e-commerce capabilities.
There are several options available with static sites, and some of them depend on where you are hosting your application.
From the CSP perspective, you can add the gatsby-plugin-csp
plugin. This plugin allows you to configure the common parts of the CSP header, but also can automatically add in the hashes for inline components as your application is built.
As an example, here's the gatsby-plugin-csp
configuration (in gatsby-config.js
) that I was experimenting with for my site.
{
resolve: `gatsby-plugin-csp`,
options: {
disableOnDev: false,
mergeScriptHashes: true,
mergeStyleHashes: true,
mergeDefaultDirectives: true,
directives: {
"default-src": "'self' https:",
"script-src": "'self' 'unsafe-inline' https:",
"style-src": "'self' 'unsafe-inline' https: blob: ",
"img-src": "'self' data: https:",
"font-src": "'self' data: https:",
},
},
},
The result is that - as above - the CSP header is added as an http-equiv
as shown above. In reality, the above example allows anything so long as it's HTTPS.
Using the _headers
file
Netlify uses a file to specify the headers that can be applied to the applications that it hosts. This file is named _headers
and sits in the publish directory of your site.
When I use this file, I place it in my static
folder. Typically, I use a fairly simple file, but it can become more complex if you need it to. Here's an example from a site I've been working on recently:
/*
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'self' https://*.kc-usercontent.com https://*.google-analytics.com; font-src data:; style-src 'self' 'unsafe-inline' https://*.googletagmanager.com; script-src 'self' 'unsafe-inline' https://*.googletagmanager.com https://*.google-analytics.com
Referrer-Policy: strict-origin-when-cross-origin
Feature-Policy: geolocation 'self'
Feature-Policy: midi 'self'
Feature-Policy: notifications 'self'
Feature-Policy: push 'self'
Feature-Policy: sync-xhr 'self'
Feature-Policy: microphone 'self'
Feature-Policy: camera 'self'
Feature-Policy: magnetometer 'self'
Feature-Policy: gyroscope 'self'
Feature-Policy: speaker 'self'
Feature-Policy: vibrate 'self'
Feature-Policy: fullscreen 'self'
Feature-Policy: payment 'self'
Read about setting custom headers in Netlify.
Setting headers in ASP.NET Core
There are a number of places that you can add security headers in ASP.NET. First, you could add them in the application configuration itself by adding the following instartup.cs
:
app.Use(async (context, next) => {
context.Response.Headers.App(
"Content-Security-Policy",
"script-src 'self'; " +
"style-src 'self'; " +
"img-src 'self'");
await next();
});
This policy simply stated that any scripts, styles, or image must come from the same origin as the application. This is going to apply the header to all requests to the application.
Another approach is to use the web.config
file as follows:
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Content-Security-Policy" value="script-src 'self'; style-src 'self'; img-src 'self'; " />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
Again, this approach will add the same CSP to all requests tot he application.
There are also external libraries that you can use to control security headers. A great example is aspnetcore-security-headers
by Joonas W (available on NuGet). It deals with a CSP, HSTS, and HTTP Public Key Pinning (HPKP) headers, but also allows you to cater for inline resources throughout the application using nonces.
How to test security headers
When it comes to testing your headers, there are two things to consider:
- Are they even there at all?
- They are there, but do they work?
Testing for the first question is simple. The simplest thing is to use the developer tools in your browser to view the response headers and see what is present - but you need to know what you're looking for.
There are also plenty of free tools available online that will scan your application and tell you which headers it has found. Two which I fall back on the most are:
The two are very similar in the way that they work. Once your scan is complete, you get more detail and a summary of the headers that have been checked. SecurityHeaders.com will give you a rating from A+ to F (and R, which I think should stand for ‘Run Away’), whereas SerpWorx gives you a score out of 100.
Checking that the headers work is a more complex task. It requires you to understand how each of the headers that you implement behaves. Once you understand that, you can create test scenarios to ensure that the headers are working.
Example of testing the Content Security Policy (CSP)
Let's consider the following HTML document:
<!DOCTYPE html>
<html>
<head>
<title>Setting CSP Reponse Header</title>
</head>
<body>
<main>
<h1 style="color:rebeccapurple">Setting CSP Reponse Header</h1>
<p>Sample page to test the CSP header</p>
<img src="https://placehold.it/600x300" alt="yup" />
</main>
<script>
(function () {
alert("Hello, World");
})();
</script>
</body>
</html>
This document currently has no security headers set. ON page load, it greets the world with a javascript popup, it uses an inline style attribute to add colour to our heading, and it displays an image from an external source. Once loaded it looks a bit like this:
What we want to do is add a basic CSP to the page to help lock things down. This can be done by adding the following to the document head
:
<meta http-equiv="Content-Security-Policy" content="
default-src 'self' ;
">
But now when we refresh our page, it looks like this:
The image is broke, the inline style is not working, and the javascript pop-up does not trigger. CHrome's developer tools tell us all about these three issues in the console log. We have three issues, so let's see how they can be addressed.
The first issue is that our inline style violates our set directive:
index.html:12 Refused to apply inline style because it violates the following Content Security Policy directive: "default-src 'self' ". Either the 'unsafe-inline' keyword, a hash ('sha256-NcqL2O3Vi61pA+um5+nt0/EJDYXH4MJIaeZnKVe0Yro='), or a nonce ('nonce-...') is required to enable inline execution. Note also that 'style-src' was not explicitly set, so 'default-src' is used as a fallback.
Because we've set this using an attribute on the h1
, we're left with only one option. We have to set unsafe-inline
in the style-src
directive.
<meta http-equiv="Content-Security-Policy" content="
default-src 'self' ;
style-src 'self' 'unsafe-inline' ;
">
But why use 'unsafe-inline'
rather than a hash or a nonce? The hash and nonce don't work for attributes, they only apply to elements. You can go ahead and try adding the hash for the inline style, but Chrome is going to ignore you.
It's worth noting that Gatsby uses a fair bit inline attributes, so those sites are going to need 'unsafe-inline'
.
The second issue that the image cannot be loaded as if comes from a different origin.
index.html:14 Refused to load the image 'https://placehold.it/600x300' because it violates the following Content Security Policy directive: "default-src 'self' ". Note that 'img-src' was not explicitly set, so 'default-src' is used as a fallback.
To resolve this, we can add https://placehold.it
to the img-src
directive:
<meta http-equiv="Content-Security-Policy" content="
default-src 'self' ;
style-src 'self' 'unsafe-inline' ;
img-src 'self' https://placehold.it ;
">
And finally, our exciting javascript alert is not working:
index.html:16 Refused to execute inline script because it violates the following Content Security Policy directive: "default-src 'self' ". Either the 'unsafe-inline' keyword, a hash ('sha256-M0EvshAmM+P/2ftO4FhRfWslEE4wBspM79DQuiCW9SE='), or a nonce ('nonce-...') is required to enable inline execution. Note also that 'script-src' was not explicitly set, so 'default-src' is used as a fallback.
In this instance, we using an inline script element, so we can add the hash to the script-src
directive to fix this:
<meta http-equiv="Content-Security-Policy" content="
default-src 'self' ;
style-src 'self' 'unsafe-inline' ;
img-src 'self' https://placehold.it ;
script-src 'self' 'sha256-M0EvshAmM+P/2ftO4FhRfWslEE4wBspM79DQuiCW9SE=' ;
">
You may notice that I've specified 'self'
on all of the directives. You don't need to do this, but its purpose is to allow that specific directive from our origin. In most cases, you'll probably trust your origin.
Once those are all saved, we can reload the page and everything works again. The difference now is that we're telling the user agent what to expect and asking it to ignore everything else.
Conclusion
Security headers perform an important role in asserting the capabilities and requirements of your application to the user agent. How you set them depends upon how your application is created and hosted, but then the result is a set of directives that the user agent should follow.
Adding the CSP will most definitely break things at first. In reality, it is supposed to. When you move to production, you may well find that you need different values - so you need to test!
Not only do these contribute to the overall security of the application, but in some cases and also help to improve SEO by contributing to faster page load times.
Top comments (1)
Fantastic article on preventing clickjacking vulnerability in Gatsby. Thanks for your hard work and detail here.