When I started thinking about building my new website, I wanted to make sure that even though its content is not sensible or private, I wanted to make sure it follows the best-in-class security practices.
In this first iteration, I didn't want to bother also managing the back-end infrastructure, so I decided to host the website in a third-party hosting provider. There are excellent options out there, but I decided to stick with Netlify for the time being, I am not using any custom feature so it would be straightforward to migrate between services.
My goal is to get the top score in the Security Headers. If you have never used this website, it is brilliant; it gives you a score and recommendations on how to improve it.
This is the score you get with the default configuration in Netlify:
Not great, isn't it? But the site informs you about what is missing and give you links to understand more:
The cool thing about Netlify is that allows you to set your custom HTTP headers effortlessly. You need to add a file named _headers
with the headers.
Make sure the file ends up in your built folder if you use Webpack or any build tool.
This is the content of the _headers
file for an A+ score:
/*
Content-Security-Policy: default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self';
Feature-Policy: accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'self'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; speaker 'none'; sync-xhr 'none'; usb 'none'; vr 'none';
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
And this is how I got an A+ score:
I will explain how did I come up with these values:
-
X-Content-Type-Options: nosniff
should always be set with that value. -
X-XSS-Protection: 1; mode=block
this one is to provide XSS protection in older browsers that do not support the Content Security Policy (CSP) standard. -
X-Frame-Options: DENY
this one is also added to protect browsers that do not support the CSP standard. The value should always be set toDENY
unless you are planning to show your site in an iframe on another website. This is very important to protect againstClickjack attacks
.
The CSP
value requires more explanation. I am not going to explain the whole CSP standard; I will focus on how do I approach setting the value.
I always start with denying any resource loading by default with:
Content-Security-Policy: default-src 'none';
And then I start adding only the necessary exceptions for the site to work. This approach is the most secure way as you don't need to fully know all the current directives available (CSP is a living standard; new directives are added regularly).
In the first iterations of this website, I only needed to be able to load scripts and images, so I add an exception for both to allow loading them only from the same domain. This means all content comes from my server and should be secure:
Content-Security-Policy: default-src 'none'; script-src 'self'; img-src 'self';
As the development of the website evolves, I will continue to add more directives. For example, at some point, I will have to allow loading styles, so I will have to change the policy to be like:
Content-Security-Policy: default-src 'none'; script-src 'self'; img-src 'self'; style-src 'self';
EDIT: There is an issue with prefetch requests. As part of CSP v3, a new directive will be introduced to define their behavior
prefetch-src
but it is not available in any browser, yet. But if you setdefault-src
tonone
, prefetch queries are all blocked by default. Why? :(. So that gives me two options only: or don't use prefetch at all, or set the default toself
... And I guess is not that bad as setting default toself
is okay, but what if I have to make a prefetch to another domain, do I need to whitelist all requests to that domain?
For the Feature-Policy
I followed the same approach: I disallow all the features by default, and I will start enabling them as I need them. Unfortunately, the current standard does not support denying all by default hence the length of the value.
And last but not least, I disabled the referred information Referrer-Policy
as I do not need it for now. I might enable it later if I need it for Analytics purposes.
Bonus: How to add custom headers to Zeit Now.
Zeit Now also supports adding custom HTTP headers. This is the content of the now.json
file to achieve the same as in Netlify:
{
"version": 2,
"name": "jvegadev",
"routes": [
{
"src": "/.*",
"headers": {
"Content-Security-Policy": "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self';",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "no-referrer",
"Feature-Policy": "accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'self'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; speaker 'none'; sync-xhr 'none'; usb 'none'; vr 'none';"
}
}
]
}
Interesting links:
Top comments (0)