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
:
1 |
Header set X-Frame-Options DENY |
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:
1 2 3 |
Header set X-XSS-Protection "1; mode=block" Header set X-Content-Type-Options nosniff |
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
:
1 2 3 4 5 |
RewriteEngine On RewriteCond %{HTTPS} !=on RewriteRule ^.*$ https://%{SERVER_NAME}%{REQUEST_URI} [R,L] |
Now we can set the HSTS header. I’m using a max-age equivalent to 30 days.
1 |
Header set Strict-Transport-Security "max-age=2592000" env=HTTPS |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Header set Content-Security-Policy " default-src 'self'; font-src 'self' data: https://fonts.gstatic.com https://themes.googleusercontent.com https://*.wp.com; script-src 'self' 'unsafe-inline' https://*.wp.com https://public-api.wordpress.com https://*.gravatar.com; style-src 'self' https://secure.gravatar.com https://*.wp.com https://*.gravatar.com 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https://secure.gravatar.com https://*.wp.com https://pixel.wp.com; frame-src 'self' https://secure.gravatar.com https://public-api.wordpress.com https://widgets.wp.com; object-src 'none' " |
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.
1 |
Header set X-Permitted-Cross-Domain-Policies "master-only" |
Here’s everything we’ve added to .htaccess
so far:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
RewriteEngine On RewriteCond %{HTTPS} !=on RewriteRule ^.*$ https://%{SERVER_NAME}%{REQUEST_URI} [R,L] Header set Strict-Transport-Security "max-age=2592000" env=HTTPS Header set X-Frame-Options DENY Header set X-XSS-Protection "1; mode=block" Header set X-Content-Type-Options nosniff Header set X-Permitted-Cross-Domain-Policies "master-only" Header set Content-Security-Policy "default-src 'self'; font-src 'self' data: https://fonts.gstatic.com https://themes.googleusercontent.com https://*.wp.com; script-src 'self' 'unsafe-inline' https://*.wp.com https://public-api.wordpress.com https://*.gravatar.com; style-src 'self' https://secure.gravatar.com https://*.wp.com https://*.gravatar.com 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https://secure.gravatar.com https://*.wp.com https://pixel.wp.com; frame-src 'self' https://secure.gravatar.com https://public-api.wordpress.com https://widgets.wp.com; object-src 'none'" |
With that .htaccess
in place, here’s what the response to a HEAD
request looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
$ curl -I https://danielnixon.org HTTP/1.1 200 OK Date: Tue, 05 Aug 2014 03:04:18 GMT Server: Apache Vary: Cookie X-Pingback: https://danielnixon.org/xmlrpc.php Link: ; rel=shortlink Strict-Transport-Security: max-age=2592000 X-Frame-Options: DENY X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Content-Security-Policy: default-src 'self'; font-src 'self' data: https://fonts.gstatic.com https://themes.googleusercontent.com https://*.wp.com; script-src 'self' 'unsafe-inline' https://*.wp.com https://public-api.wordpress.com https://*.gravatar.com; style-src 'self' https://secure.gravatar.com https://*.wp.com https://*.gravatar.com 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https://secure.gravatar.com https://*.wp.com https://pixel.wp.com; frame-src 'self' https://secure.gravatar.com https://public-api.wordpress.com https://widgets.wp.com; object-src 'none' X-Permitted-Cross-Domain-Policies: master-only Content-Type: text/html; charset=UTF-8 |
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
:
1 |
Header set Content-Security-Policy "default-src 'self'; font-src 'self' data: https://fonts.gstatic.com https://themes.googleusercontent.com https://*.wp.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.wp.com https://public-api.wordpress.com https://*.gravatar.com; style-src 'self' https://secure.gravatar.com https://*.wp.com https://*.gravatar.com 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https://secure.gravatar.com https://*.wp.com https://pixel.wp.com; frame-src 'self' https://secure.gravatar.com https://public-api.wordpress.com https://widgets.wp.com; object-src 'none'" |
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?
Hi,
Thank for your article, i already implement to my website except the “Content-Security-Policy” part.
Regards.
How can i set csp for non https header so that if i forget using non-https image, css or script, they won’t be load?
Really nice article very easy to understand, Thanks for sharing
Daniel, thank you for sharing your knowledge.
In addition to the headers discussed by you, you might be interested in how to setup the HPKP (Public-Key-Pins) header. See http://zinoui.com/blog/security-http-headers
great! Thanks for sharing this tut! my site mark B hehe
You should use apache httpd’s “Header always set” otherwise in case of HTTP status > 200 the headers aren’t set and someone can still exploit you by finding an 400 bad request/500 internal server error – which is very easy in apache.
Thank you for your article, it helps me a lot