HTTP headers for improved security

Post #4 published on by Tobias Fedder

The website has been online for a few days. Getting there involved some arduous tasks — looking at you privacy policy. Now it's time for some low‐hanging fruit. I'll add four headers to the responses send from my webserver to increase its (percieved) security.

First of them is the X-Content-Type-Options header. I set it to nosniff, the only possible value. This header tells the browser to take the MIME-type, declared in the Content-Type header, seriously. If, for example, the webserver declares that a response contains text/css, the browser shouldn't analyze the file and take a guess what to do with it but instead just treat it as the kind of content that was declared. It's an important setting for websites that allow uploads and downloads of user content, where users could upload malicious, executable files that other users' browsers would download, then recognize as an executable and run. So far there is no user content on this website, but in case that changes one day, I can't forget to set that header anymore. Until then it checks a box on security assessments that are done from the outside.

An actually useful header is the HTTP Strict-Transport-Security header (HSTS). There a max-age in seconds defines for how long the browser should enforce HTTPS for all requests to that domain. Furthermore the directives includeSubDomains and preload can be set. After receiving a first response with that header set, the browser will rewrite all request beginning with http: to https: within the given time frame. With each response containing that header, the time frame will be extended. In case I make a mistake when linking a resource, or someone provides a link to this site, but the URL starts with http:, the browser will intervene and use https: instead. To get that security enhancement even before the first response, there is a long, long, very long, list of domains with HSTS. That list is maintained by Google, shared with other browsers as well. The preload directive I mentioned is a way to get on that list.

What if the browser rewrites everything to https: but some things are served exclusively via http:? In short: It breaks.
Long answer: It breaks and you can't do anything about it except wait for the clock running out.
And that is why I choose to start with the duration of one day for the max-age, and leave out the preload for now. I can't think of any resource served on this domain, including all subdomains, using http:. But in case I did miss one, I'd rather wait a day then, say, a year or two. Assuming everything goes well, I'll increase the time frame, probably to a year, and consider again to put this domain on that HSTS preload list.

Next up is another old header starting with X. X-Frame-Options is a header to signal to browsers whether or not a page is allowed to be displayed in a frame, such as an <iframe>. If so, the URIs of the parents that'd be allowed can be defined. I set it to DENY. The purpose is to prevent so called clickjacking. So far I don't have interactive elements that an attacker could consider worth tricking you into clicking, but again: better safe than sorry. In browsers that support the Content-Security-Policy header, which is true for most of them, the X-Frame-Options header is obsolete.

The Content-Security-Policy header is the last on my list. It enables webservers to declare very granulary, which origins are allowed to load certain kinds of resources. For convenience there is a directive called default-src that many kinds of resources fall back on when not set explicitly. I'll make use of that and will allow the origin 'self', which is the domain that serves this header. With that most kinds of resources are allowed, including frames, as long as I get them from my website's origin. An exception to that is the CSS that I so far just dump into a <style> in the <head> of all HTML files. That is inline CSS and therefore needs to be allowed separately. Here the additions to my Caddyfile:

tfedder.{$TLDOMAIN:de} {
⋮
header {
	Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'"
	Strict-Transport-Security "max-age=86400; includeSubDomains"
	X-Content-Type-Options nosniff
	X-Frame-Options DENY
	⋮

Stay safe.