Security by HTTP headers
Some HTTP security policies are as simple as adding a header to the response. It's common to just add it to your configuration, but did you actually check if this is working as you expected? For Nginx, a relatively popular and commonly used web server, this might seem surprisingly easy, but there's a huge pitfall.
I'll show you in this blog post it's easy to end up with an insecure configuration which you may look good from looking at the server configuration. It's about the add_header
Nginx configuration directive that handles scoping completely different from what you may expect.
While I'm not the only one running into it and there are plenty of troubleshooting topics for this directive indicating this pitfall, I still see bad examples online. Lately when I made the same mistake, a colleague noticed we weren't doing HSTS (HTTP Strict Transport Security) anymore after deploying a change involving caching headers. This made me write this up to raise some attention.
By example; Clickjacking protection and HSTS.
Let's go over this by example. Your site is TLS-enabled (HTTPS), it is clickjacking protected, it is HSTS enabled and you're confident it will pass the security scan. Below is the basics of the Nginx configuration for such, as you may consider sensible.
# IMPORTANT! BELOW IS UNSAFE. DON'T COPY-PASTE ME. READ THE BLOG POST.
http {
# Clickjacking protection, see:
# https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
add_header X-Frame-Options SAMEORIGIN;
# Serve HTTP non-TLS
server {
listen 80;
server_name www.example.com;
...
}
# Serve HTTPS
server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate www.example.com.crt;
# Enable HSTS, only for HTTPS!
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
root /srv/web/www.example.com/http_root;
# To my dynamic web application (e.g. fcgi, uwsgi, ...)
...
location /api/sensitive {
# Responses contain sensitive data; browsers and proxy servers should
# not cache any of this.
add_header Pragma "no-cache";
add_header Cache-Control "private, max-age=0, no-cache, no-store";
}
location /static {
alias /srv/web/www.example.com/static;
# This content never changes; aggressive caching enabled.
add_header Pragma "cache";
add_header Cache-Control "public";
}
...
}
}
Clickjacking protection header applied globally in the configuration, check. HSTS header present and only on HTTPS, check. And sensitive data is not cached or stored, check.
Now, you're going to test the output in the browser.
Does it actually respond with all the headers you would expect?
Let's test this with curl
:
# HTTP
$ curl -Is http://www.example.com/ | grep -F X-Frame-Options
X-Frame-Options: SAMEORIGIN
# HTTPS
$ curl -Is https://www.example.com/ | grep -F X-Frame-Options
What?
We've defined the X-Frame-Options
on the http
scope that covers both the HTTP and HTTPS server
scopes, right?
The answer is, yes, but the add_header
for HSTS in the server
scope has cleared the X-Frame-Options
header in its parent scope.
But, really? It's a totally unrelated header!
Yep. It's behaviour as documented:
There could be several add_header directives. These directives are inherited from the previous level if and only if there are no add_header directives defined on the current level.
Same goes for the caching headers in the HTTPS server
block:
# HSTS works for HTTPS, yey!
$ curl -Is https://www.example.com/ | grep -F Strict-Transport-Security
Strict-Transport-Security: max-age=31536000; includeSubDomains
# Also when accessing any actual content?
$ curl -Is https://www.example.com/static/main.js | grep -F Strict-Transport-Security
This could mean that if the user is not actually accessing any content outside the unprotected URIs, he will effectively not see any HSTS protection. For this example configuration it could be a low impact, but for a scope breaking it that covers most of the requests, it could be very harmful!
Possible solutions
Alternative module for setting headers
The ngx_headers_more plugin will by default preserve headers added in the parent scope.
Procedures to install this unofficial plugin may not be a solution for everyone, although it is available in Debian/Ubuntu via the nginx-extras package. It also requires to change existing configurations.
Define a common config snippet
Create files to include always when fiddling with headers. For example:
In http_headers.conf
:
add_header X-Frame-Options SAMEORIGIN;
In https_headers.conf
:
include http_headers.conf
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
In your site configuration:
http {
include http_headers.conf;
# Serve HTTP non-TLS
server {
listen 80;
...
}
# Serve HTTPS
server {
listen 443 ssl;
...
include https_headers.conf;
...
location /api/sensitive {
# Responses contain sensitive data; browsers and proxy servers should
# not cache any of this.
add_header Pragma "no-cache";
add_header Cache-Control "private, max-age=0, no-cache, no-store";
include https_headers.conf;
}
...
}
}
It will work, it's "copy-paste safe", I'd say, but it has some drawbacks:
- It suddenly breaks when someone adds a
add_header
statement in the firstserver
scope. - Quite some extra configuration overhead.
Bad examples in the public
- GitHub Gist Best nginx configuration for improved security(and performance) All headers in the parent scope are not effective by the HSTS header, as also noted in a comment there.
- Misleading and wrong, but accepted answer on StackOverflow on the question why the
add_header
isn't working in the child scope. - Horde's nginx example configuration.
Share your thoughts
Have a better solution? Please share it below in the comments!
Also shocked? Feel free to retweet. ๐
Using Nginx? You should know about the add_header directive pitfall.https://t.co/7MMyuJ6YOn
— Gert van Dijk โ ๏ธ (@gertvdijk) February 17, 2016