HTTP security headers

I’ve been willing to write about HTTP security headers for a long time, but never took the time to actually do it. Well, here it is. I’m going to keep it concise, while still attempting to explain each header and its options. ## Wait, HTTP headers what? Your website is under attack, right now. You surely have heard about CSRF, XSS, clickjacking, … If not and you are hosting a website, please go attend a web security course.

If you have, you know that you should have secured your website against these attacks. You also know that was a very difficult task. You’re also not 100% certain that your input filters block all XSS attacks. Or you are just looking to use as much of the defense mechanisms as possible.

Say hi to the security HTTP response headers!

HTTP headers can do hell of a good job to add an extra layer of security to your web app. It is certainly not a replacement for writing secure code, but it provides for a nice added risk mitigation layer. Let’s cut straight to the chase.


Quick summary

For those with not a lot of time, here’s a summary of all security headers:

Content-Security-Policy: "default-src 'self'; script-src 'self' 'sha256-YOkIld+4YPqI/bL2YwQmHJ879igiUq4Zv/WhvPU2FwY=' 'sha256-Xlomi/q6HfCY3VR51ECFtpqgNC3yE6WknLM6m1yXBwU='"
Strict-Transport-Security: "max-age=31536000; includeSubDomains; preload"
Public-Key-Pins: "pin-sha256=\"Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=\"; pin-sha256=\"YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=\"; pin-sha256=\"ekONRpysxjd9Ev3d7tBCBhRUFA9u9wZY75TjnomW+0w=\"; pin-sha256=\"p7ZRf3+KxYW1jSSYlvzIl9m8G2UXRxFy8ivMeJqJ234=\"; max-age=5184000;"
X-FRAME-OPTIONS: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Expect-CT

Verify browser support on caniuse.com. For example, for CSP: https://caniuse.com/#feat=contentsecuritypolicy2.


Content-Security-Policy

BAM. One hell of a security header to start with. Sadly, also one that you won’t have up and running in 2 minutes.

In an XSS attack, an attacker will try to inject Javascript in your application. Roughly two ways exist to inject scripts on a page: 1. inline (<script>alert('test');</script>) 2. by referencing a URI (<script src="http://badattacker.com/istealyourcookie.js"></script>)

The idea of the CSP header is fairly simple: block inline scripts and block all sources from unknown domains. Moreover, CSP does not just protect against Javascript injection. It also protects against maliciously loaded css files, images, fonts, etc. For simplicity, I’ve just set a ‘default-src’. It tells my browser to only load references from the ‘self’ domain, being my own domain. You can have more fine-grained control by using other directives, for a full list see here.

CSP version 1

Content-Security-Policy: default-src 'self'; 

Test it! Your webpage will probably be broken. Most likely because of the fact that you have quite some inline scripts in your webpage, which are now blocked by the browser (hit F12 to see the errors in the developer console). Now, there isn’t a way to let your browser distinguish between ‘good’ inline scripts and ‘bad’ inline scripts, is there? In version 2, there is:

CSP version 2

Content-Security-Policy: "default-src 'self'; script-src 'sha256-GAF48QOoxRvu0gZAmQivUdJPyBacqznBAXwnkfpmQX4='"

I told my browser to accept the inline script alert(‘test’); by hashing it: echo -n "alert('test');" | openssl dgst -sha256 -binary | openssl enc -base64 and putting it in the CSP-header! CSP version 3 will even allow to hash externally referenced scripts, more details here.

Awesome, isn’t it? You can have as many ‘sha256-’ directives as you want. One for each script or style block that you wish to allow.


Strict-Transport-Security

Users browse to websites in several ways: * by typing the url (e.g. “infosecmike.com”) directly in the address bar * by googling * by using favorites

Let’s consider the first case. The owner of infosecmike.com has adequately configured his website with HTTPS, and so the following process takes place:

     +-------+                                         +-------+
     |Browser|                                         |Server |
     |-------|         infosecmike.com(:80)            |-------|
     |       |+--------------------------------------->|+-----+|
     |       |                                         | 302  ||
     |       |              redirect to                |      ||
     |       |<---------------------------------------+|<-----+|
     |       |    https://infosecmike.com(:443)        |       |
     |       |                                         |       |
     |       |                                         |       |
     |       |                                         |       |
     |       |           secured traffic               |       |
     |       |+--------------------------------------->|       |
     |       |                                         |       |
     |       |<---------------------------------------+|       |
     |       |                                         |       |
     +-------+                                         +-------+

Even though your browser hides the port number in the address bar, it tries to connect port 80, which is the default port for plain-text HTTP traffic. A request to the server takes place, to which the server answers with a redirect to HTTPS (default port 443). The browser then continues using HTTPS. The connection is secure only from the second HTTP request onwards! An attacker could simply do the following:

     +-------+                                         +-------+
     |Browser|                                         |Server |
     |-------|         infosecmike.com:80              |-------|
     |       |+--------------------------------------->|+-----+|
     |       |                                         | 302  ||
     |       |              +--------+   redirect to   |      ||
     |       |              |Attacker|<---------------+|<-----+|
     |       |              |--------|     https       |       |
     |       |              |        |                 |       |
     |       |              |        |                 |       |
     |       |              | Strip  |                 |       |
     |       |  unsecured   | HTTPS  | secured traffic |       |
     |       |              |        |+--------------->|       |
     |       |              |        |                 |       |
     |       |<------------+|        |<---------------+|       |
     |       |              |        |                 |       |
     +-------+              |        |                 +-------+
                            |        |
                            |        |
                            |        |
                            |        |
                            +--------+

The gist of the attack is that the attacker sets up a secured connection with the server, and that you set up an unsecured connection with the attacker. Your browser thinks a plain-text HTTP session is just fine, because it never received the redirect to HTTPS.

To solve this, the browser should immediately try HTTPS. However, we can’t do that for websites which are not HTTPS enabled, because then these sites would be broken. We need a way to tell the browser which websites are HTTPS-only, and that’s basically all the HSTS-header does.

Strict-Transport-Security: "max-age=31536000; includeSubDomains; preload"

Now I tell the browser to: * Never try to connect via plain HTTP traffic for a period of 31.536.000 seconds (about 1 year) * Do that for all subdomains of infosecmike.com * Submit my HSTS for manual review so it will be included in the preloaded browser list

This last point is a special one: by using this header, my application is still exposed to the attack explained above the first time a user connects to my website. However, the exposure is very short: once the user has received this header, he is safe for 31.536.000 seconds. If I want my users to be safe immediately (i.e. before their first connect), I should include my website in the HSTS-list which comes preinstalled with the browser. This is what the preload option tries to achieve.

You can check these lists, by the way: * Chrome: go to chrome://net-internals/#hsts and query a domain * Firefox: type %APPDATA%\Mozilla\Firefox\Profiles\ in file explorer, double click your profile folder, and open the file SiteSecurityServiceState.txt.


Public-Key-Pins

This is an interesting one. Many people are afraid of this header and advise against using it, a simple search on the Internet would reveal that. However, I think it’s great. Recently I’ve been working on a PKI project (next blog post coming up), and I realized once again that PKI is a house of cards ready to collapse at any moment. We put trust in certificate authorities, which have the power to issue certificates for just any domain. The fact that this trust is not always legitimate, is confirmed by Google who regularly decides to distrust some shabby CAs, like Startcom or even Symantec.

Essentially, public key pinning allows you to select one or two CAs in which you put your trust, without having to trust all CAs that are in the OS or browser by default. You could even decide to trust just one specific certificate (actually, you trust the public key inside the certificate, but that’s a technical detail).

I admit, it’s also fairly dangerous. Putting your trust in one CA implies that you will be in deep trouble when that CA decides to go out of business. Trusting just one certificate is extremely dangerous: you’ll be in deep trouble when for some reason you have to change your public key and you didn’t plan for it. Anyway. It’s still great, when done right. The idea behind is pinning is very simple. Pinning a public key is like telling your browser to expect that exact public key somewhere in the certificate chain. If it’s not there, the browser will refuse the connection. It does so for as long as I tell the browser it must do so.

Two rules: 1. Never pin exclusively to one CA 2. Always have a back-up plan

Usually, a chain will count 2, 3, or 4 certificates (but it could be more, does not really matter). For example:

 +----------+         +----------+         +----------+
 |  root    |  signs  | intermed |  signs  |   leaf   |
 |          |+------->|          |+------->|          |
 |  PK1     |         |   PK2    |         |   PK3    |
 +----------+         +----------+         +----------+

This chain counts 3 certificates. The leaf certificate is the one that was created for your domain, the root certificate is the one that is trusted by your OS or browser, and the intermediate certificate is the link between the two. Now, each of these certificates contains a public key (PK). It is this PK that can be pinned. If you pin PK1, you basically tell your browser to trust all certificates that start from that root (e.g. Globalsign). If you only pin PK3, you tell your browser to only trust that particular leaf certificate.

I’ll show you what I did:

 +----------+         +----------+         +----------+
 |  root1   |  signs  | interm1  |         |   leaf   |
 |          |+------->|          |+------->|          |
 | PK1: PIN |         | PK2: PIN |         |   PK3    |
 +----------+         +----------+         +----------+
 ------------------------------------------------------
 +----------+
 |   CSR1   |
 | back-up  |
 | PK4: PIN |
 +----------+
 ------------------------------------------------------
 +----------+
 |   CSR2   |
 | back-up  |
 | PK5: PIN |
 +----------+

I pinned 4 public keys. Two of them are present in the live chain, two of them are pins of public keys for which I have a corresponding private key stored safely in an offline back-up. If my complete live chain goes down and pin 1 and pin 2 are gone somehow, I can get one of the back-up CSRs from my shelf, submit it to any CA, and install the certificate they give me. That certificate will then contain PK4, and the chain would look like this:

 +----------+         +----------+         +----------+
 |  root2   |  signs  | interm2  |  signs  |   leaf   |
 |          |+------->|          |+------->|          |
 |   PK6    |         |   PK7    |         |  PK4:PIN |
 +----------+         +----------+         +----------+

See, it will still be accepted by the browser, as PK4 is in the list of known PINs. Of course, I would probably want to start including PK6 and PK7 in my pins now. One last important note: just as with HSTS, you can specify a max-age. This tells the browser for how long it should enforce those particular pins. The longer you set it, the more you have to be careful with changing public keys.

Enough with the theory, this is my set-up:

Public-Key-Pins: "pin-sha256=\"Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=\"; pin-sha256=\"YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=\"; pin-sha256=\"ekONRpysxjd9Ev3d7tBCBhRUFA9u9wZY75TjnomW+0w=\"; pin-sha256=\"p7ZRf3+KxYW1jSSYlvzIl9m8G2UXRxFy8ivMeJqJ234=\"; max-age=5184000;"

To calculate the PIN, you can use a comparable command that we used for creating the CSP-hash:

For a CSR

openssl req -pubkey < backup.csr | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64

For a PEM certificate

openssl x509 -pubkey -noout < letsencryptroot.pem | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64

I have put my max-age on 60 days (5 184 000 seconds).

Check your configuration In Chrome, you can check the pins for any website by typing chrome://net-internals in your address bar, and then the ‘HSTS’ tab. Query your domain, and you should see a result. Try querying www.infosecmike.com and you should my pins. Another nice website to check your configuration is report-uri.io.


X-Frame-Options

Every heard of clickjacking? Let me try to visualize the situation:

         +--------------------------------------------------------------------+
         |                         100prices.com                              |
         |--------------------------------------------------------------------|
         |                                                                    |
         |                                                                    |
         |                                                                    |
         |                                                                    |
         |             +---------------------------------------+              |
         |             |   iframe src: www.infosecmike.com     |              |
         |             |---------------------------------------|              |
         |             |                                       |              |
         |             |                                       |              |
         |             |                                       |              |
         |   +---------|                      +---------+      |---------+    |
         |   | Click he|                      | Delete  |      | now!    |    |
         |   |         |                      +---------+      |         |    |
         |   +---------|                                       |---------+    |
         |             |                                       |              |
         |             +---------------------------------------+              |
         |                                                                    |
         |                                                                    |
         |                                                                    |
         +--------------------------------------------------------------------+
         

So, you stumbled upon a website named 100prices.com. I don’t judge you, I know you visited that page just by accident. Anyway, you see a nice website with a button. That button says: Click here to claim your price now!. What you don’t know is that another website was loaded through an iframe. Whereas in my visualization the iframe is visible, the attacker makes sure the iframe is 100% opaque: you do not see it thus you do not know that another website was loaded within the malicious website.

Now it just happens to be so that the Click here to claim your price now-button is positioned exactly below a delete-button on the iframed website. However, as the iframe is opaque, the only thing you see is that fake price button. The browser doesn’t care that it is opaque, and will believe you pressed the delete-button instead…

Some very simple code to quickly see if your website is vulnerable:

<!DOCTYPE html>
<html>
<body>
<button type="button" style="position:absolute; left:100px; top:100px">Click here to claim €200</button>
<iframe width="600" height="200" src="https://www.infosecmike.com" style="opacity:0.2;"></iframe>
</body>
</html>

Your browser should block this iframe attempt, and the reason should be displayed in the console (press F12 to open the console). So, another header you should set:

X-FRAME-OPTIONS: DENY

Possible values are: * DENY: deny all framing attempts * SAMEORIGIN: allow framing attempts originating from the same domain * ALLOW-FROM https://example.com/ : allow framing attempts from https://example.com/


X-Content-Type-Options

This is one of the more archaic security headers. Some browsers (older IE versions, someone?) try to sniff the type of a file before rendering it to the user. Imagine that you provide the possibility to your users to upload raw text-files. An attacker could try to upload a text file and make it look like a HTML-file. When this file is rendered to other users, and their browser sniffs the file to determine that it is a HTML file, the raw text file content will be loaded as HTML, including scripts. Bad idea. To protect your users, just set:

X-Content-Type-Options: nosniff

X-XSS-Protection

One of the most vague headers that’s out there, and it only works in IE/Edge. It’s proprietary Microsoft technology to prevent XSS attacks. Not much is known about this header, you can find some details here. In any case, it shouldn’t hurt to set it:

X-XSS-Protection: 1; mode=block

Referrer-Policy

This is a brand new header which mainly protects the privacy of your users. Usually, when a user comes to my website, he arrives there via some other site (e.g. via a link on Facebook). It is useful for me to know that user came via Facebook. It is also nice for me to know that you were looking at my profile before you clicked the link (the referrer is https://www.facebook.com/michael.boeynaems) You might not like that, so Facebook could therefore set the new header to make sure no referrer is sent.

Referrer-Policy: no-referrer

However, this might be a bit overkill. It sounds reasonable to send along https://www.facebook.com as a referrer, while omitting the michael.boeynaems path:

Referrer-Policy: origin-when-cross-origin

This tells the browser to only send the origin (https://www.facebook.com) in the referrer.

The complete list of possible options can be found on W3C: * no-referrer: always omit the referrer * no-referrer-when-downgrade: omit the referrer when linking from a HTTPS domain to a HTTP domain, otherwise send full referrer (browser’s default) * same-origin: include full referrer on same domain, otherwise omit completely * origin: always send origin in the referrer * strict-origin: omit the referrer when linking from a HTTPS domain to a HTTP domain, otherwise send origin * origin-when-cross-origin: include full referrer on same domain, otherwise only send origin * strict-origin-when-cross-origin: include full referrer on same domain, send only origin to other domains, omit referrer when linking from a HTTPS domain to a HTTP domain. * unsafe-url: always send full referrer

My preferred choice:

Referrer-Policy: strict-origin-when-cross-origin

X-Download-Options

Again, one of the archaic ones. When you download a file, you can generally choose to open,save, or save as the file. Turns out that in IE8, the open option would run the file in the same origin as the original website. This is a nice website where a possible attack against this is demoed. Newer browsers just make sure downloaded files run in a kind of sandbox, but a header too much never killed nobody:

X-Download-Options: noopen

X-Permitted-Cross-Domain-Policies

I hope you have heard of the Same Origin Policy which is enforced by default in a web browser. Sometimes, you want to circumvent this policy (e.g. if you want to get information from an API hosted on a different subdomain). You can use CORS to carefully punch holes in the Same Origin Policy.

To make sure the Same Origin Policy is enforced for clients other than a browser (e.g. a PDF file, a Flash application, etc.), you can set a cross-domain-policy. This can be set by means of an XML file, or just by setting a simple header. In my case, I do not want any PDF file to access my resources:

X-Permitted-Cross-Domain-Policies: none

Expect-CT

This one is not even out yet. As soon as it is official, I’ll add it here. In the meanwhile check here for the draft.

VAT: BE0693954727