Control security response headers with ExpressJS and HelmetJS
Background
We, the frontend developers of Springer Nature’s Institutional Customer Experience (ICE) team, control several Express web servers that provide the web pages to our customers.
Recently, I randomly checked one of the websites using the online service provided by Security Headers. Security Headers is a web service that checks the response headers sent by a web server for security-related issues and provides a rating from A+ (best) to F (worst). For each security-related header that is present or absent, it provides further details to give users the information they need to increase the security level of their web server.
The results it gave for the server I tested were not too bad, but could be improved (we got a D grade on a scale of A++ to F).
It took me several tries to get the result I wanted. Along the way, I learned more about web server security which I would like to share in this article.
Upgrading the Helmet module
It turned out that we were already using Helmet, a plugin for the Express web server that helps secure it by setting HTTP response headers.
Helmet sets the response headers it deems necessary to provide an up-to-date and solidly secured web server as far as it’s response headers are concerned.
The reason our server scored so poorly in the test was mainly because the version of Helmet we were using was out of date.
The current version would automatically take care of the most important missing headers - we were using version 3.x
, yet the current major version already reached 7
!
However, without manual tuning, the website would most likely fail to load external resources such as scripts, images and frames. It would also not allow any inline scripts to run in the browser, which is a sensible basic setting. Bear in mind that you can use any of these security headers without losing the desired level of security - you can configure fine grained exceptions for every content security policy directive.
Manual modification of the default settings
One of the most important settings to change, unless you host all the resources yourself, is the Content Security Policy (CSP).
An excerpt from the Helmet website:
To configure this header, pass an object with a nested directives object. Each key is a directive name in camel case (such as defaultSrc) or kebab case (such as default-src). Each value is an array (or other iterable) of strings or functions for that directive. If a function appears in the array, it will be called with the request and response objects.
A good way to find out which setting your website needs is to simply use the default Helmet setup, e.g.
and then run the server and examine the error messages that are likely to appear in the browser console.
In this article I will go step by step through some of the problems I found on our server.
Content Security Policy (CSP) directives
The first error that was displayed in the console said (disclosed details have been replaced by the letters XYZ in this article):
So this message already tells us that Helmet by default sets the Content Security Policy directive "script-src 'self'"
, but it seems that this is not enough, as here we want to load a JavaScript file from a domain that we trust. So we need do explicitly define the values for the CSP script-src
directive:
By adding the value *.springernature.com
, we allow all subdomains of springernature.com
to be used in the src
attributes of our <script>
tags.
We can do this quite safely because we trust our subdomains to serve scripts; so even if a hacker could trick our pages into loading JavaScript from some untrusted domain, those scripts would not execute.
Note that the 'self'
entry in the above example is enclosed in two sets of quotes, the inner ones being single quotes.
This is necessary because predefined values (keywords) that can be used in the different CSP directives must be supplied in single quotes according to CSP specifications.
When not used, the keywords will be considered as a part of a resource URL - URLs do not use any quotes.
Therefore, we add our new entry here using just a pair of (double) quotes.
After this update and a recompile of our code, the server now loads the external script without complaint.
But there was also another error message in the console output, which looked a bit different:
This time the error complains about an inline script that the browser will not run because of the CSP. This prevents the execution of inline scripts that a malicious user might embed in the pages our web server serves. I think this is a good attitude of the default CSP settings, but we have an inline script that we want the browser to run.
Not understanding how to use the hash or a nonce here, I simply added the suggested ‘unsafe-inline’ keyword to our script-src
directive.
Since it’s a predefined term for the script-src
values, it also needs to be enclosed in single quotes.
I’ll come back to this later in the article.
Our helmet configuration now looks like this:
After these changes, the external scripts could be loaded and the inline scripts were executed by the browser.
The next error that appeared in our particular case was:
In this case, it was a script we hosted ourselves (so the source was trustworthy), but it used an eval
command, which is generally considered to be potentially harmful and should be avoided whenever possible.
So the best way to deal with this problem would be to get rid of the eval
command, but that is beyond the scope of this article.
Instead, I will describe how I first dangerously decided to allow the use of eval' (and [some other commands that are prevented](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#unsafe_eval_expressions) unless
‘unsafe-eval’ has been added to your script-src' directive).
But be aware that you generally should not allow the use of
eval, and later in this document, I fix my previous decision.
The error message already tells us that we need to add
‘unsafe-eval’ to our
script-src` directive to get rid of the error:
This was all it took to get rid of all our `script-src’ directive errors.
Next came a slightly different error message:
Again, this error is related to a src
, but in this case not for a <script>
tag, but for some script interface trying to load data from Google Analytics, which we also use on this site.
Again, this error message tells us about our options to get rid of the problem.
We could either extend the default-src
directive or use the more specific connect-src
directive.
Since changing the default-src
directive would allow more than just using the call to Google Analytics through a connect-src
, we will explicitly configure the connect-src
:
Now that the browser is allowed to connect to Google Analytics (GA) via a script interface, a new error occurs as Google Analytics seems to be trying to load an image from the Google Analytics website.
Although we have allowed the connect-src
to be www.google-analytics.com
, we have not allowed an image source to be anything other than 'self'
.
If we had allowed the GA domain for the more general default-src
, then this would also include the domain to be used as img-src
.
But we want to be strict and have full control over what a browser is allowed to load or not, which is why we add an entry for the img-src
as well:
We got rid of all the error messages for now, but of course we need to check all the pages we have. And indeed, a new error appeared on another page:
This image URL comes from a background SVG image that we have defined in our CSS using a data URI. Even this is prevented by the Content Security Policy directives that Helmet sets by default, so we need to allow it for us as an image source as well:
Note that 'data:'
is also one of the predefined values (including the trailing colon), so it must be added to the values array in single quotes.
Having done this, I proceeded to the next pages and another error appeared on the console:
Yes, we use the Surveymonkey service on one of our pages to display a form.
Surveymonkey does this by injecting an iframe into our page, and this requires the Content Security Policy directive frame-src
, which is also covered by the more general default-src
directive.
But we don’t want to use the general default-src
directive, as I explained earlier, but rather maintain finer-grained control.
So we will now configure the frame-src
directive as well:
These were the Content Security Policy directives. But Helmet offers more security related settings, so I’ll explain what we configured in addition.
Frameguard / X-Frame-Options
Helmet offers a setting called frameguard
.
This setting allows you to specify values for the X-Frame-Options
header that will prevent the web page that comes with this header from being embedded in a frame by another web page. This is intended to prevent “Clickjacking” attacks.
Currently it takes one of two values: deny
or sameorigin
.
If you do not change this setting, then by default Helmet will send this header with the sameorigin
value, allowing sites with the same origin as the current site to include it in a frame.
If you want to prevent even that, then set this to deny
:
Now, any browser that supports this header will never display our site in a frame, no matter where. Be aware, though, that this then prevents your customers from viewing your website within an iframe. Therefore, you should eventually clarify this point so that you do not lock your customers out. That being said, the technique of integrating a foreign website inside an iframe and make it look as if it were original, is a well-known technique used by hackers. Through this technique they can take control of the actions of your users, whilst said users believe they are simply browsing your site as normal.
HTTP Strict-Transport-Security (HSTS)
Then there is an option that tells the browser to automatically prefer HTTPS over HTTP. It tells browsers that the site should only be accessed via HTTPS and that any future attempts to access it via HTTP should automatically be converted to HTTPS.
The header responsible for this is called Strict-Transport-Security and you should definitely set it.
In the Helmet setup, you can use the strictTransportSecurity
or hsts
attributes interchangeably.
The hsts entry lets you can set the following attributes:
maxAge
- the number of milliseconds the browser will remember to only request data from your site using HTTPSincludeSubDomains
- a boolean value that tells the browsers whether to include subdomains to respect the strictTransportSecurity rule as wellpreload
- a boolean value (see explanation below) Thepreload
attribute is not a standard, but a service provided by Google that modern browsers use. The supporting browsers preload a list of domains to request data from using HTTPS, even if they have never accessed those servers before. I recommend to use this value as well.
According to the documentation on the Mozilla Developer Network, the value for maxAge
should be 63072000 (2 years), so we use that:
This is now a good setup for our security-related headers - almost.
How to get rid of the unsafe-inline
setting
Remember I said I was going back to the unsafe-inline
option I had originally used because I did not understand the other two options the error message suggested I use instead?
I felt really bad about this “unsafe” setting, and it definitely is unsafe, so let’s now look at the two ways to avoid it.
Use a hash value in the script-src
directive
The first alternative to using `unsafe-inline’ that the error message suggested was to use a hash. What’s even better: It provided the exact hash to use, so I can copy & paste and make our configuration look like this:
Note that the hash value must also be inserted into the array with single quotes around it, as suggested by the error message in the console. Like for the keywords, when you leave off the single quotes, the hash would be interpreted a part of a URL.
There are several different hash algorithms that produce hashes of different lengths.
For our purposes, the SHA-256 algorithm is good enough, as it is used for creating a one-way hash, not for encryption.
This 256 bits long hash value is calculated from every character between the opening and closing <script>
tags of the inline script in question.
There are online tools that will calculate this value for you, for example this inline script and style hasher.
But there is a caveat: you have to use the complete contents of your script tag(s) (including white spaces) after compiling and possibly minifying your HTML.
Otherwise, you would get the hash of the unminified source, which would not match the minified script code on your production website and thus the CSP would not allow it’s execution.
This makes it a bit cumbersome to maintain - after every single change, even a whitespace, you have to recalculate the script hash.
I think it’s acceptable to use the value given in the error message in the console that you see after making a change to your code. So if you use the hash method, you need to make sure that you either always check the pages of your site and look for error messages in the browser console, or you automate this process in some way. This could be done passively by automating the check for errors or actively by calculating the hashes of the compiled scripts and inserting them into the CSP settings. Both methods are not trivial and beyond the scope of this post.
Use a nonce in the script-src
directive
A Content Security Policy “nonce” is a randomly generated token that is used for exactly one request - so a new nonce has to be generated for every single request.
If you want to use a nonce, you need to set up a logic that automatically generates a new nonce per inline script for each request, and ensure that all these nonces are added to the response headers sent by your server. This is not a trivial task, but once it’s set up you won’t have to recalculate the hash value needed when using hashes instead.
Also note that this will prevent your pages from being cached, as each request will result in a different response.
Report only
One caveat of using a Content Security Policy on your server is that if you accidentally miss an entry for any of the directives - e.g. because it only takes effect on a production system - then things you need might fail to load.
Sometimes this can be very hard to test yourself - for example when loading a third-party resource behind a CDN.
Let’s say you are based in Italy. You may have blocked all domains by default (good!) but allowed it.example.net
because that’s the domain the third-party CDN redirects your request to. However a user in Germany may be making the request of de.example.net
and their request would fail because you block all domains by default.
This would be very hard to spot without using a CSP report server.
Therefore, it’s helpful to simulate the CSP and collect the errors it would yield if it were not simulated.
For this use case, a special response header exists: The Content-Security-Policy-Report-Only.
With Helmet, it is very easy to use this variant of the Content Security Policy: You simply add the reportOnly
directive to it with the value of true
.
In our example from above, changing the CSP to only “simulate” it’s settings, looks like this:
Yet, adding the reportOnly
setting doesn’t help - it would simply result in making the configured CSP useless, as it is then only simulated and there is no setup for where the report should go to.
The Content-Security-Policy-Report-Only
response header needs a report-to /csp-violation-report-endpoint/
addition, where the /csp-violation-report-endpoint/
part is a relative or absolute URL the report will be sent to in JSON format.
Unfortunately, the developer of the Helmet module decided to not support the report-to
entry, as it is not strictly related to improving security. But there is a plugin called “report-to” that can be used in addition the the CSP setup in Helmet. See it’s documentation for more details.
In order to make use of this feature, you need to setup a script that captures this JSON data and stores it for further inspection. Alternatively, you can use a paid service, for example Report-URI. Such a service may offer a free trial period; which may suffice for testing whether your CSP settings are good enough.
Check your setup
One last thing I want to mention: You regularly should check your CSP in production using an online service such as the CSP-evaluator. It may come up with valuable hints on weaknesses in your setup and how to improve (and why).
In summary
- If you use Express to serve your website, you should also use the Helmet plugin in order to improve the security of your website against hacking attacks.
- Rely on the defaults that Helmet provides until you encounter issues that require specific settings in your Content Security Policy. Helmet provides the best practices for you.
- Always make sure you are using the latest version of Helmet, as new best practices will emerge in the future.
- Adopt a “deny-by-default” mentality; block everything and only allow what you need. Be as specific as possible, e.g. by not allowing all subdomains of a particular domain using the asterisk (*) wildcard, and by restricting access to the full domain names that you know your site needs to access.
- Be aware that your rules may need ongoing maintenance if you are using inline script securing with hashes.
- If you disable Helmet for in development environments, be sure to turn it on and test your site by looking at the browser console output before deploying.
- Be aware that there are sometimes issues in production that you may not see in development or staging environments, such as when the Google Tag Manager tries to dynamically load pixel images only in production.
- Use the Content-Security-Policy-Report-Only header in the beginning to check if your CSP would break anything on your production server.
Happy securing.
Useful links
- Express, a well-known web server for NodeJS
- Helmet, a plugin for Express that adds security related response headers
- Content Security Policy (CSP), a good documentation of the CSP on the Mozilla Developer Network
- “Clickjacking” attacks, a documentation from the Open Worldwide Application Security Project (OWASP) online community
- Content Security Policy “nonce”, a documentation explaining the meaning and use of “nonce”s
- report-to, a plugin for Helmet adding a
report-to
setting to theContent-Security-Policy-Report-Only
response headers - Report-URI, an online service for collecting data sent through a
Content-Security-Policy-Report-Only
response header setup - Security Headers, an online scanner for your security headers
- CSP-evaluator, a very useful tool for finding flaws in your CSP
Find this post useful, or want to discuss some of the topics?
About The Authors
-
Roman Seidelsohn
Check out Roman's open-source project on GitHub