Using HTTP Headers to Secure Your Sites

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.

Test Scores

Test Pass Score Explanation
Content Security Policy -25 Content Security Policy (CSP) header not implemented
Cookies 0 No cookies detected
Cross-origin Resource Sharing 0 Content is not visible via cross-origin resource sharing (CORS) files or headers
HTTP Public Key Pinning 0 HTTP Public Key Pinning (HPKP) header not implemented (optional)
HTTP Strict Transport Security -20 HTTP Strict Transport Security (HSTS) header not implemented
Redirection 0 Initial redirection is to https on same host, final destination is https
Referrer Policy 0 Referrer-Policy header not implemented (optional)
Subresource Integrity 0 Subresource Integrity (SRI) not implemented, but all scripts are loaded from a similar origin
X-Content-Type-Options -5 X-Content-Type-Options header not implemented
X-Frame-Options -20 X-Frame-Options (XFO) header not implemented
X-XSS-Protection -10 X-XSS-Protection header not implemented

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.

Content Security Policy (CSP)

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:

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.

HTTP Strict Transport Security (HSTS)

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:

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

X-Content-Type-Options

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"
  }
}

X-Frame-Options

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"
  }
}

X-XSS-Protection

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"
  }
}

Putting it all together

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!

The Results

Test Pass Score Explanation
Content Security Policy +5 Content Security Policy (CSP) implemented without 'unsafe-inline' or 'unsafe-eval'
Cookies 0 No cookies detected
Cross-origin Resource Sharing 0 Content is not visible via cross-origin resource sharing (CORS) files or headers
HTTP Public Key Pinning 0 HTTP Public Key Pinning (HPKP) header not implemented (optional)
HTTP Strict Transport Security 0 HTTP Strict Transport Security (HSTS) header set to a minimum of six months (15768000)
Redirection 0 Initial redirection is to https on same host, final destination is https
Referrer Policy 0 Referrer-Policy header not implemented (optional)
Subresource Integrity 0 Subresource Integrity (SRI) not implemented, but all scripts are loaded from a similar origin
X-Content-Type-Options 0 X-Content-Type-Options header set to "nosniff"
X-Frame-Options +5 X-Frame-Options (XFO) implemented via the CSP frame-ancestors directive
X-XSS-Protection 0 X-XSS-Protection header set to "1; mode=block"

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

Extra Credit

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

Referrer Policy

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 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

More?

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.

Final Result

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.

{
  "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';",
      "Referrer-Policy": "no-referrer, strict-origin-when-cross-origin",
      "Strict-Transport-Security": "max-age=63072000; includeSubDomains",
      "X-Content-Type-Options": "nosniff",
      "X-Frame-Options": "DENY",
      "X-XSS-Protection": "1; mode=block"
    }
  }
}