Using HTTP Headers to Secure Your Site

Listen to this article

Observatory by Mozilla helps websites by teaching developers, system administrators, and security professionals how to configure their sites safely and securely.

Let's take a look at the scores Observatory gives for a fairly straightforward Static Buildpack app, https://2017.keeprubyweird.com.

Even though we use Heroku's Automated Certificate Management to easily get an SSL certificate for our domains, our overall score is an F, 20/100. We'll walk through each failing test, learn what caused the failure, and try to fix them.

The failure here is "CSP header not implemented", and when we view the linked security guideline we see that CSP gives us control over where scripts and resources we reference on our site can be loaded from. For Keep Ruby Weird, this means fonts, several external image sources, and a couple of analytics sources.

CSP gives us a few levels of strictness:

default-src <source> , script-src <source> , object-src <source> , etc. These limit the sources of various types of resources. https: limit resources of the specified type, or all resources, to HTTPS only https://example.com limit resources to this domain. Multiple such sources can be provided for the same *-src directive. 'self' means that resources can only be loaded from the current host, useful for relative resources like <script src="/index.js"> . See CSP: default-src on MDN for full options here.

, , , etc. These limit the sources of various types of resources. The Content-Security-Policy header disallows <script> tags with inline code by default. Those with a src instead are allowed. This can be disabled by adding 'unsafe-inline' which makes our site less secure. You can also specify nonce s or SHA sums of the content of those scripts to allow them to execute.

header disallows tags with inline code by default. Those with a instead are allowed. This can be disabled by adding which makes our site less secure. You can also specify s or SHA sums of the content of those scripts to allow them to execute. frame-ancestors 'none' Prevents your site from being loaded in an iframe and being used in a clickjacking attack. If you do need to display your site in an iframe, you can specify URLs instead.

For our purposes, we will want to be able to use self-hosted resources, images from Twitter and AWS, scripts from Google and Twitter analytics, and both a stylesheet and font entry from Google Fonts. We'll also need to re-define 'self' in a few directives because they don't fall back on the default unless the options aren't specified. For example, we haven't specified object-src so it falls back on the default-src value of 'self' .

Content-Security-Policy: default-src 'self'; script-src https://static.ads-twitter.com https://www.google-analytics.com; img-src 'self' https://s3.amazonaws.com https://twitter.com https://pbs.twimg.com; font-src 'self' https://fonts.gstatic.com; style-src 'self' https://fonts.googleapis.com; frame-ancestors 'none';

We can add this to our static.json as a part of the headers collection for all paths:

# ... "headers": { "/**": { "Content-Security-Policy": "default-src 'self'; script-src https://static.ads-twitter.com https://www.google-analytics.com; img-src 'self' https://s3.amazonaws.com https://twitter.com https://pbs.twimg.com; font-src 'self' https://fonts.gstatic.com; style-src 'self' https://fonts.googleapis.com; frame-ancestors 'none';" } }

When we add or remove external resources, we'll need to update this collection or our users will see errors in the browser's console and the resources will be unavailable.

We failed this test for basically the same reason: "HTTP Strict Transport Security (HSTS) header not implemented".

HSTS tells a browser that our site should only be viewed over HTTPS. Looking at the HSTS security guideline, we see that HSTS provides several nonexclusive flags:

max-age=<seconds> . How long user agents will redirect to HTTPS, in seconds. This tells a browser "once you've seen this, assume that all requests to this domain will be over HTTPS for this long." Mozilla recommends 2 years, or 63072000 seconds. This flag is required.

. How long user agents will redirect to HTTPS, in seconds. This tells a browser "once you've seen this, assume that all requests to this domain will be over HTTPS for this long." Mozilla recommends 2 years, or seconds. This flag is required. includeSubDomains . Whether user agents should upgrade requests on subdomains. Unless you have a reason you wouldn't have SSL on all subdomains, you probably want this. I'd recommend it even if you don't currently have subdomains.

. Whether user agents should upgrade requests on subdomains. Unless you have a reason you wouldn't have SSL on all subdomains, you probably want this. I'd recommend it even if you don't currently have subdomains. preload . If you have this flag and also register your domain on the Chrome HSTS preload list, browsers will not even need to see this header before forcing HTTPS requests. This is useful, but opt-in as you do need to register your own domain by looking it up on the preload list, checking some boxes, and submitting. It's pretty easy and we'll do it here.

# ... "headers": { "/**": { "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload" } }

This header tells browsers not to load scripts and stylesheets if their MIME type as indicated by the server is incorrect. It's a good thing to have on.

# ... "headers": { "/**": { "X-Content-Type-Options": "nosniff" } }

This header prevents your site from being loaded in an iframe. It helps prevent "clickjacking" attacks. It is the same protection offered by frame-ancestors 'none' in Content-Security-Policy but adds support for older browsers. If you do need to display your site in an iframe on another page of your site, you can instead use the SAMEORIGIN option.

# ... "headers": { "/**": { "X-Frame-Options": "DENY" } }

This header protects from cross-site scripting (XSS) attacks. It provides similar protection as Content-Security-Policy but again protects older browsers.

# ... "headers": { "/**": { "X-XSS-Protection": "1; mode=block" } }

Adding this header block to our static.json increases our score from an F to an A on the Observatory.

"headers": { "/**": { "Content-Security-Policy": "default-src 'self'; script-src https://static.ads-twitter.com https://www.google-analytics.com 'sha256-q2sY7jlDS4SrxBg6oq/NBYk9XVSwDsterXWpH99SAn0='; img-src 'self' https://s3.amazonaws.com https://twitter.com https://pbs.twimg.com; font-src 'self' https://fonts.gstatic.com; style-src 'self' https://fonts.googleapis.com; frame-ancestors 'none';", "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "X-XSS-Protection": "1; mode=block" } }

When we load up the browser, everything looks right! Unfortunately we did miss one thing, which we can see in the console.

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src https://static.ads-twitter.com https://www.google-analytics.com". Either the 'unsafe-inline' keyword, a hash ('sha256-q2sY7jlDS4SrxBg6oq/NBYk9XVSwDsterXWpH99SAn0='), or a nonce ('nonce-...') is required to enable inline execution.

Even though we added https://www.google-analytics.com to our script-src, because it is being loaded in an inline script we'll need to allow it to be run explicitly. The error message is kind enough to offer us a couple of options: "Either the 'unsafe-inline' keyword, a hash ('sha256-q2sY7jlDS4SrxBg6oq/NBYk9XVSwDsterXWpH99SAn0='), or a nonce ('nonce-...') is required to enable inline execution."

'unsafe-inline' sounds, well, unsafe, so let's skip that. A nonce is a one-time-use number that would allow the inline script to be run. Nonces can be made safe, but as we're talking about static pages that's out of scope here. The hash provided in the error message is the actual sha-256 sum of the content of the inline code block, is more secure than the other options. It will keep attackers from changing the content of the Google Analytics inline script which makes it safer than an unprotected inline script. Like changing external dependencies, if we ever change that script tag we'll also need to change the sha-256 sum.

"Content-Security-Policy": "default-src 'self'; script-src https://static.ads-twitter.com https://www.google-analytics.com 'sha256-q2sY7jlDS4SrxBg6oq/NBYk9XVSwDsterXWpH99SAn0='; img-src 'self' https://s3.amazonaws.com https://twitter.com https://pbs.twimg.com; font-src 'self' https://fonts.gstatic.com; style-src 'self' https://fonts.googleapis.com; frame-ancestors 'none';"

We've added the SHA sum, and now Google Analytics is all set up!

We're up to A+! Not bad for an hour's work, and our users are much more secure now when visiting our site.

We're on the home stretch now. There are a few optional things we can do to beef up security and privacy even more.

Browsers include a Referrer header that identifies where a user came from when visiting a new page. It's useful in tracking where users are coming from, but there are some privacy concerns with that. The Referrer-Policy header controls when and how much information is provided.

no-referrer . Tells the browser to never send the Referer header.

. Tells the browser to never send the header. same-origin . Send the referrer, but only on requests inside the site (e.g. /security-in-the-static-buildpack => /posts)

. Send the referrer, but only on requests inside the site (e.g. /security-in-the-static-buildpack => /posts) strict-origin . Send the referrer information to all origins, but only the URL sans path (e.g. https://example.com/)

. Send the referrer information to all origins, but only the URL sans path (e.g. https://example.com/) strict-origin-when-cross-origin . Send full referrer information on same origin, but only the URL sans path on foreign origin.

no-referrer can be used as a fallback for browsers as many of these options have not yet been implemented at this point.

Referrer-Policy: no-referrer, strict-origin-when-cross-origin

Mozilla Observatory also has tests for Cookies and Subresource Integrity, but it was happy with the Keep Ruby Weird site after the changes we've already made so those are left as an exercise for the reader.

Here is the final result of this change, excluding the opt-in "preload" directive to HSTS, and it is our recommendation for all static buildpack apps.