Making sure that the application is secure is one of the most important tasks of a developer. This is a vast topic, and in this article, we tackle it by adding a set of HTTP headers.
The Helmet is a middleware for Express applications. It sets many different HTTP headers and aims to make applications more secure. Adding it to our application is very straightforward:
1 |
app.use(helmet()); |
The above is a wrapper for eleven smaller middlewares. Using it sets quite a bit of different HTTP headers for us. Let’s break this set of middleware one by one.
Content-Security-Policy
The purpose of the Content-Security-Policy header is to add a security layer reducing the risk of attacks such as Cross-Site Scripting (XSS). With it, we state which dynamic sources the browser is allowed to load.
The person performing the XSS attack exploits the fact that the browser can’t tell the difference between a script that is a part of our application and the one injected maliciously. When we define a Content-Security-Policy header, we can tell the browser which URLs to trust.
1 |
Content-Security-Policy: script-src 'self'; |
The header in the above example tells the browser not to load any scripts from origins different than the opened page. It also blocks all inline scripts and inline event handlers. If you want to use external sources of code such as CDN, you can specify its URL here.
There are many more rules than we can specify, and the Helmet middleware sets quite a lot of them. Running app.use(helmet()), or app.use(helmet.contentSecurityPolicy()), gets us the following:
1 2 3 4 5 6 7 8 9 10 11 |
default-src 'self'; base-uri 'self'; block-all-mixed-content; font-src 'self' https: data:; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests |
If you want to check all available policies, MDN is a great resource.
X-DNS-Prefetch-Control
While DNS requests cost little bandwidth-wise, the latency might be significant. If we can predict which hostnames will be needed, we can perform a lookup beforehand.
1 |
<link rel="dns-prefetch" href="https://www.reddit.com/"> |
Thanks to doing the above, the www.reddit.com domain is resolved before it is needed.
Unfortunately, some attackers might take advantage of the DNS prefetching. The X-DNS-Prefetch-Control header is responsible for controlling the DNS prefetching feature. If we don’t utilize it, we might as well turn it off to improve the security of our page slightly. This is the default behavior of app.use(helmet()) and app.use(helmet.dnsPrefetchControl()).
Expect-CT
The Certificate Transparency (CT) is an open-source framework for monitoring digital certificates. Public logs aim to contain all certificates issued by trusted certificate authorities. This allows for the identification of maliciously created certificates.
By enabling the Expect-CT header, the site requests that the browsers check if the site’s certificates appear in public CT logs. The header might become obsolete in June 2021 because, at that point, every certificate will be expected to be submitted to the CT logs.
We can set this header either with app.use(helmet()) or app.use(helmet.expectCt()).
Referrer-Policy
The browser sends the address of the page through the Referer request header. For example, when our application makes HTTP requests to the API, the Referer header contains the URL of the application.
The Referer header is actually a misspeling of the word referrer.
The Referer header might be useful for various things such as loggings and analytics. Unfortunately, it might also lead to malicious data tracking or information leakage. Due to that, browsers send the Referer when an HTTPS site tries to communicate with an HTTP destination.
With the Referrer-Policy header, we have the opportunity to specify the desired behavior. We have quite a few options to choose from. However, running app.use(helmet()) or app.use(helmet.referrerPolicy()) sets the Referrer-Policy header to no-referrer. It gets rid of the Referer header completely from requests.
If we need the Referer header, we can pass some additional arguments to the Helmet middleware.
Strict-Transport-Security
When we visit a site using HTTPS and the Strict-Transport-Security header, the browse remembers it. If we ever try to load the same site through HTTP, we are automatically switched to HTTPS.
This might prevent a sort of the man-in-the-middle attack. The hacker can intercept our HTTP request and redirect us to a clone of the site we intend to use. This can lead to exposing very sensitive data. If the Strict-Transport-Security header is used, the above scenario is impossible if we visited the original site at least once.
If we use app.use(helmet()) or app.use(helmet.hsts()), there is a set of default options chosen for us. For example, the header takes an effect for 180 days and includes subdomains.
X-Content-Type-Options
Browsers can fetch many different resources for an application, such as stylesheets, images, and JavaScript files. Each of those should include a Content-Type response header. An example of a valid header value is image/png or text/css.
Unfortunately, sometimes the value of the Content-Type can be wrong or be missing. In this situation, the browser can perform MIME sniffing and attempt to guess the correct MIME type based on the resource.
This might prove to be problematic. When our site allows some data to be uploaded to the server, the attacker can disguise an HTML file with the MIME type of image/png, for example. If our users late fetch this file, it opens up a possibility for an XSS attack.
To avoid that, app.use(helmet()) and app.use(helmet.noSniff()) set the X-Content-Type-Options header to nosniff. This causes the browser not to analyze the file. Therefore, we need to make sure that we always send the correct Content-Type response header.
X-Download-Options
With the Helmet middleware, we even have some improvements for Internet Explorer 8. The issue is when the IE8 user downloads an HTML file and chooses to open it directly instead of saving it to drive. This causes the HTML file to execute in the context of the website the user downloaded it from.
Setting the X-Download-Options header to noopen prevents the IE8 users from directly opening a file before download. We can achieve this with app.use(helmet()) or app.use(helmet.ieNoOpen()).
X-Frame-Options
Clickjacking is an attack that utilizes iframes. Imagine the attacker preparing a website with some buttons. On top of them, a transparent iframe is placed that renders our page that we want to protect.
The attacker can line up the invisible iframe to line up with some of the buttons on the malicious website. When the victim tries to click them, it results in clicking on an invisible button instead. This hijacks the user’s click, and this is why we call it clickjacking.
With the X-Frame-Options header, we can tell the browser whether it can render the page in an iframe. The default value set by app.use(helmet()) and app.use(helmet.frameguard()) is SAMEORIGIN. This causes the webpage to displayable in iframes on the same origin as the page itself. We could also use DENY to block our page from being displayed in iframes completely.
This applies also to the <embed>, <object>, and <frame> elements.
X-Permitted-Cross-Domain-Policies
Software such as Adobe Flash Player and Adobe Acrobat can embed content from websites in documents. They block all cross-domain requests by default, such as browsers block cross-domain requests. This can be changed by providing a prepared crossdomain.xml policy file. If an attacker finds a way to inject this file, it changes the cross-domain policy for some Adobe clients.
A way to override that is to provide the X-Permitted-Cross-Domain-Policies set to none. We can do it by running app.use(helmet()), or app.use(helmet.permittedCrossDomainPolicies()).
X-Powered-By
The X-Powered-By is a response header that specifies the technology behind our application. If you are using Express, all endpoints return the X-Powered-By: Express header by default.
It is always better if our potential attackers don’t know the technology we are using. Even though there are more ways for the attackers to identify that we use Express, the Helmet library decided to remove this header by default. It happens when we run app.use(helmet()) or app.use(helmet.hidePoweredBy()).
If you want to know more about the discussion about removing this header from Express, check this GitHub issue
X-XSS-Protection
The X-XSS-Protection header is a feature in some older browsers. It aims to stop a page from loading when a browser detects an XSS attack. Nowadays, this is no longer necessary, especially with the use of the Content-Security-Policy header. Also, it turned out that it causes some issues. All modern browsers aside from Safari no longer support this functionality.
By running app.use(helmet()) or app.use(helmet.xssFilter()) we set X-XSS-Protection to . This disables this functionality for browsers that still support it.
Summary
In this article, we’ve gone through all of the headers that the Helmet library sets up for us. We’ve described what each one of them does and why. All of them help us make our application a little bit more secure. Adding those headers can help us avoid attacks such as XSS or clickjacking. Caring for that aspect of an app is a significant part of the job of every developer.