HTTP Security Headers

In this post I’ll discuss the HTTP headers we can use to improve a web site’s security and mitigate certain attack types. I’ll use this blag as my example.

First up we’ll look at X-Frame-Options. This header helps prevent clickjacking by indicating to a browser that it shouldn’t render the page in a frame (or an iframe or object). We’ll use the strictest setting: DENY. Here’s how to set it in .htaccess:

Next we’ll add two Internet Explorer-only headers: X-XSS-Protection and X-Content-Type-Options.

X-XSS-Protection helps mitigate Cross-site scripting (XSS) attacks. We’ll use the strictest setting again here: “1; block”. This will instruct IE to not even render the page if it detects an XSS attack.

X-Content-Type-Options only has one value“nosniff”which instructs IE not to sniff mime types, preventing attacks related to mime-sniffing. You should be fine to use this header unless you’re serving files with bad Content-Type headers (don’t do that).

Here’s what we need to add to .htaccess for these two headers:

Now we’ll take a look at HTTP Strict Transport Security (HSTS). HSTS is a way for the server to instruct the browser that it (the browser) should only communicate with the server over HTTPS. This helps avoid man-in-the-middle attacks over insecure HTTP. The first thing we need to do is ensure that we’re redirecting all HTTP connections to the equivalent HTTPS URL. Here’s one way to achieve that in .htaccess:

Now we can set the HSTS header. I’m using a max-age equivalent to 30 days.

We must not send this header over plain HTTP, so we add env=HTTPS.

Next we’ll implement the Content Security Policy (CSP) header. CSP helps mitigate XSS attacks by whitelisting the allowed sources of content such as scripts, styles and images. Using WordPress 3.9.1, Accessible Zen 1.1.3 and Crayon 2.6.5, this is as tight as I could make the CSP header for this blag. I’ve inserted line breaks for readability.

Some observations:

  • We block all objects (such as Flash) with object-src ‘none’. Good riddance.
  • We unfortunately have to allow inline scripts and styles using ‘unsafe-inline’ for both script-src and style-src.
  • We only allow https and data schemes, we don’t allow any insecure http.

Finally we’ll look at X-Permitted-Cross-Domain-Policies. Setting this header to “master-only” will instruct Flash and PDF files that they should only read the master crossdomain.xml file from the root of the website.

Here’s everything we’ve added to .htaccess so far:

With that .htaccess in place, here’s what the response to a HEAD request looks like:

Unfortunately, something in the WordPress backend uses eval, so we have to include the ‘unsafe-eval’ source in our CSP script-src directive. Fortunately, we can get away with only including ‘unsafe-eval’ in an .htaccess in the wp-admin directory. This setup allows eval in the WordPress backend but blocks it for frontend pages. The CSP header in wp-admin’s .htaccess adds ‘unsafe-eval’ but is otherwise identical to that in the root .htaccess:

And that’s it for security headers on danielnixon.org. With the unfortunate exception of those unsafe-inline and unsafe-eval sources in the CSP header, these six headers lock things down pretty well. It’s worth stressing that none of this is a replacement for writing secure code in the first place; think of these headers as just another layer of protection.

As an aside, Play Framework’s SecurityHeadersFilter provides all the above (except HSTS), with each header defaulting to a secure default value.

While we’re on the topic of HTTP headers and security, there are also some headers that should be removed. Server and X-Powered-By are two common headers that reveal information about a website that could be useful to attackers, so we should remove them if we can. Unfortunately, we’re stuck with the Server header on Apache. What we can do is shrink it down to simply “Apache”, omitting the Apache version, the OS, the OS version, etc.

You can test your own site’s security headers at https://securityheaders.com/. This site scores 90%, with that pesky Server: Apache header costing me 10%. Maybe you can beat me?