As the web keeps growing, so do the challenges that we need to undertake in order to maintain a high level of trust and security for our Web Applications. Recently browser vendors have been implementing great new features based on the W3C specs to provide better and more advanced tools to allow us, developers, to keep up with those challenges.

In this article, we are going to look at some of those tools and show you how much you can achieve with minimal effort. If you’re familiar with the Pareto principle (also known as the 80/20 rule), that’s exactly what I’m talking about.

NOTE: even though this article is focusing on the aspects from the perspective of a JavaScript Single Page Application, many of the tools and concepts can be applied to any Website or Web Application.

Table of Content

The article covers the following sections. If you are already familiar with some of those topics, feel free to “jump” to the section of your interest.

Common vulnerabilities

Protecting against XSS

Security Headers (CSP & SRI, HSTS, HPKP)

Securing sensitive information

Protecting agains CSRF

CORS

Conclusions & References

Common vulnerabilities

When we think about Web Security, probably the first thing that comes to mind is HTTPS, which is a protocol that describes and ensures that the data exchanged over the connection is securely encrypted.

But is that enough? Unfortunately, it’s not.

Two of the most common vulnerabilities for Web Applications are XSS and CSRF (I assume that you have a vague understanding of them, otherwise there are some useful references at the end of the article).

When we talk about vulnerabilities and attacks from malicious parties, it’s all about data, more specifically sensitive data, and how we keep it protected. This can be anything like a session token, a user email, etc. The browser offers different ways to store and exchange data, each one comes with pros and cons.

General overview of storing sensitive data with the related vulnerabilities.

Here at commercetools my team and I are working on a Web Application called “Merchant Center”. From a technical point of view, it’s a JavaScript Single Page Application that runs on the browser and connects to a proxy API in order to consume different services within our platform. When implementing a Single Page Application, it’s common practice to store an authorization token in window.localStorage so that it can be retrieved when sending an API request.

Unfortunately window.localStorage was never designed to be secure. Any piece of JavaScript can read and write from/to it, which opens up an XSS vulnerability attack (like a “malicious” script that reads from window.localStorage and sends data to an external “malicious” server).

In the last weeks, we’ve been digging more into this security topic and made some simple changes to raise our security defenses to a higher level. Today we want to share this experience with you, hoping that you will feel more comfortable in understanding and implementing security measures with just a few steps.

Protecting against XSS

In a JavaScript application, an XSS (Cross-Site Scripting) attack is one the most malicious attacks. Fortunately, browsers offer and implement several features to help prevent those vulnerabilities.

First, we’re going to look at some Security Headers.

A security header is “just” an HTTP Response Header specifically designed for security purposes. There are a few of them which are worth mentioning.

Content Security Policy (CSP)

The CSP header describes a series of directives (or rules) to instruct the browser to allow or block resources to be loaded and executed.

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

Overview of the most important directives for CSP.

I would argue that this is one of the most important Security Headers to use. However, it can be quite challenging to properly set it up. Let’s have a look at how you can get started with it and implement a basic CSP.

1) Start with a default template

Content-Security-Policy:

default-src 'none';

script-src 'self';

connect-src 'self';

img-src 'self';

style-src 'self';

2) Use -Report-Only mode so that the policy is active but the browser does not block anything, instead it will send violation reports to the provided URI

Content-Security-Policy-Report-Only:

default-src 'none';

script-src 'self';

connect-src 'self';

img-src 'self';

style-src 'self';

report-uri: https://sentry.io/xxx/csp-report;

3) Inspect policy violations (within the reporting tools of your choice, for example we use Sentry)

An example of a policy violation report from Sentry.

4) Update the policy to white-list the domains and resources that you trust

Content-Security-Policy-Report-Only:

default-src 'none';

script-src 'self';

connect-src 'self' https://www.google-analytics.com;

img-src 'self';

style-src 'self';

report-uri: https://sentry.io/xxx/csp-report;

5) Repeat steps 3 and 4 until you covered all the minimal rules to make your application work. To further help this process, 3rd party services like Intercom provide a list of directives to white-list all their resources.

6) Enforce the policy (replace Content-Security-Policy-Report-Only with Content-Security-Policy )

Content-Security-Policy:

default-src 'none';

script-src 'self' 'unsafe-inline' 'unsafe-eval';

connect-src 'self' https://www.google-analytics.com;

img-src 'self';

style-src 'self';

report-uri: https://sentry.io/xxx/csp-report;

Once you have your base policy enabled you can start tweaking it. One thing I would like to focus on is related to the script-src directive.

As you might have noticed, I used unsafe-inline and unsafe-eval which basically allows any kind of injected script to be executed. This might seem acceptable, depending on the type of application, but we can do better. In fact, I strongly recommend trying to get rid of those.

Safe inline scripts

Let’s look at a common example: Google Tag Manager. To set it up, you need to put an inline script in your HTML page.

<script>

dataLayer = [{ 'gtm.start': new Date().getTime() }];

</script>

Of course, you can decide to put it into a JS file, load the file instead and white-list the URI where the file lives (at the cost of an additional network request). However, for the sake of the example let’s assume we want it as an inline script.

In order to white-list that inline script, we simply need to hash it.

> require('crypto')

.createHash('sha256')

.update("dataLayer=[{'gtm.start':new Date().getTime()}];")

.digest('base64');

'gNc6N+b7YhV0rkQNN7PDbQXK1JJ70pky2rQYS6w0czM='

The CSP supports 3 types of hash algorithms: sha256, sha384, sha512.

Once you have the hash, you can white-list it in your CSP.

Content-Security-Policy:

default-src 'none';

script-src 'self' 'unsafe-eval' 'sha256-gNc6N+b7YhV0rkQNN7PDbQXK1JJ70pky2rQYS6w0czM=';

connect-src 'self' https://www.google-analytics.com;

img-src 'self';

style-src 'self';

report-uri: https://sentry.io/xxx/csp-report;

That’s it, simple as that.

NOTE that the hash is static and can be hard-coded in your CSP. If the inline script changes, you obviously need to update the hash (the browser will “remind you” of that 😉 because it simply won’t execute the script). In rare cases, you might need to dynamically generate the hash on each request, based e.g. on some environment variables. If you have control over the HTTP server you can still do that, otherwise, you need to find a different solution.

UPDATE: As an alternative of the “sha-hash”, you can also use a cryptographic “nonce-hash”. The difference is that the “nonce-hash” is uniquely generated by the server on each request and does not need to hash the script content. This is useful for more traditional Web Applications where there might be a lot of inline scripts on different pages and hashing each one of them becomes complex and tedious.

Using a “sha-hash” will allow to be more strict in which inline scripts are allowed. Using a “nonce-hash” will provide more flexibility to allow any kind of inline script rendered by the server and prevent unknown script injection.

Safe eval 😈

Well, that’s fairly easy: do not use eval 😇

Seriously, it’s really discouraged!

Furthermore, inspect your 3rd party dependencies and look for the usage of eval() or Function . If you find any, open an issue at the related library to address the problem.

Browser support

There are different versions of the CSP spec, the current one being version 2.0 with the upcoming 3.0 (still in draft). All major browser vendors support most of the features, with the exception of IE (it only supports 1.0 with the sandbox directive, nothing else 🙃).

What happens when a browser does not support a directive? Nothing, really. The unsupported directives will simply be ignored. This is a good thing for us developers because we just need to maintain one policy. On the other hand, if such an “old” browser is used, the user will simply not benefit from the security features.

Subresource Integrity (SRI)

We just looked at the Content Security Policy as a way to white-list resources that we trust to be loaded and executed.

Some of those resources might come from a CDN. We might trust the CDN domain or URI but can the files be fully trusted? 🤔

If an attacker is able to modify the e.g. jquery.min.js file content that is stored on a CDN, our application will still load and trust the file but it doesn’t know that the content changed and that it might contain malicious code.

In order to prevent that, we can use another important security feature: Subresource Integrity.

Using SRI enables browsers to verify that files they fetch (for example, from a CDN) are delivered without unexpected manipulation.

Again, this is also pretty simple to implement. We hash the file and reference the hash in the script file.

Then we enable the require-sri-for script directive in our CSP.

Content Security Policy is a powerful feature. It gives us a lot of options and flexibility to instruct the browser to understand the context of our application or website and take necessary actions. In other words, it allows you to control pretty much everything that’s going on in the browser (scripts, fonts, images, network requests, etc).

In this article we just looked at some of the features of CSP. However, there is much more to it and I recommend to follow the reference links to dig deeper.

What else can we do?

HTTP Strict Transport Security (HSTS)

The HTTP Strict Transport Security is another HTTP response header that tells the browser that it should only allow HTTPS requests on all subsequent requests. Enforcing this header highly reduces possible MiTM attacks.

Strict-Transport-Security: max-age=31536000

If you want to know more about it, I really recommend reading this article from the well-known Security Researcher, Scott Helme.

HTTP Public Key Pinning (HPKP)

This one is more of a “nice-to-have” header. It’s a very powerful one but quite delicate to handle at the same time. You probably want to look at this only in cases where you need a high level of security, such as a bank website.

So what is it? It stands for HTTP Public Key Pinning and has the ability to instruct the browser to verify a set of given cryptographic public keys against the requesting website. Enforcing this header also highly reduces possible MiTM attacks with forged certificates.

Public-Key-Pins: pin-sha256=”XXX” max-age=5184000

However, there are some important trade-offs to be aware of before using this. Again, you can read more about this in this article from the well-known Security Researcher, Scott Helme.

Securing sensitive information

As we saw in the previous sections, Security Headers do not take much effort to implement and offer a built-in high level of protection from the browser. However, in the case that an attacker still manages to bypass those security protections, our sensitive data might still be at risk.

What else can we do to store that data more securely? The answer is: use HTTP Cookies, specifically in Secure and HttpOnly mode.

Set-Cookie: Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly

The Secure flag ensures that the browser sends the cookie only over a HTTP connection.

The HttpOnly flag prevents any JavaScript code to read from it.

Simple as that…or is it? 🤔 Well, using HTTP Cookies opens up another vulnerability, namely CSRF.

Protecting against CSRF

A CSRF attack (Cross-Site Request Forgery, also pronounced as “sea-surf” or “csurf”) has been around longer than XSS and traditionally applies to normal Web Applications (using server-side rendering and forms). It’s the ability to perform unwanted actions on an authenticated user’s behalf

To understand how we can prevent this within a Single Page Application, we need to look at CORS first.

CORS

CORS stands for Cross-Origin Resource Sharing and it outlines a policy that defends against one origin from “stealing” another origin’s data.

This is a common thing to encounter if you develop a JavaScript application that makes network requests to other origins. In order for the request to “succeed”, the second origin server needs to consent the request via the Access-Control-Allow-Origin header in the response.

When does a request need the consent from the server?

Well, it depends whether the browser is forced to check if the CORS protocol is understood by the server or not. The check is performed via a “preflight” request with the HTTP OPTIONS method. The check logic is determined by the “kind” of the request: Simple or Non-Simple.

A simple request is a request that does NOT trigger a “CORS-Preflight” and it must fulfill the following requirements:

Method: GET , HEAD , POST

, , Headers: Accept , Accept-Language , Content-Type (*), …

, , (*), … Content-Type (*) values: application/x-www-form-urlencoded , multipart/form-data , text/plain

Simple requests allow CSRF attacks in case the affected origin performs authenticated requests using credentials stored as e.g. HTTP Cookie or Basic Authentication.

So how can we protect from CSRF attacks? We use Non-Simple requests!

A non-simple request is a request that TRIGGERS a “CORS-Preflight”. As opposed to a simple request, it fulfills other requirements:

Method: PUT , DELETE , OPTIONS

, , Headers: same as for simple requests, plus any custom header (e.g. X-Something )

) Content-Type values: all other values that are not included in simple requests (e.g. application/json )

As you might have noticed, requests to a JSON API will send the Content-Type: application/json header, making it a Non-Simple request.