Fighting cross-site-scripting (XSS) with content security policy


The security of our web application should be one of our primary concerns as developers. One of the threats we need to consider is cross-site scripting (XSS). This article explains the danger it poses and how we can fight it using a Content Security Policy (CSP) header.

Cross-Site Scripting (XSS)

With cross-site scripting (XSS) attacks, an attacker injects malicious code into our website. It often takes the form of JavaScript code that can harm our users when it runs in their browser. For example, imagine an attacker injecting the following script into the website:

Assuming the above request is not blocked by the Cross-Origin Resource Sharing mechanism, it sends the cookies of our users to the attacker.

If you want to know more about Cross-Origin Resource Sharing (CORS), check out Cross-Origin Resource Sharing. Avoiding Access-Control-Allow-Origin CORS error

The above can be a massive problem if we store sensitive data in cookies such as authentication tokens. Therefore, we should mark sensitive cookies with the HttpOnly flag so that no one can access them through the property.

If you want to know how to design an authentication that uses HttpOnly and the Set-Cookie header, check out API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies

When attackers access cookies, they can impersonate our users in a similar way they would when stealing a password.

Content Security Policy (CSP)

Above, we’ve mentioned two security mechanisms that would stop the attacker from retrieving the cookies. However, if they fail for some reason, we can still rely on the content security policy if we set it up correctly.

Content security policy allows us to control what resources the browser can load and execute. Let’s look at a straightforward example with images.

If matches our content security policy, the browser displays it as usual. Otherwise, the browser does not request it, and therefore it does not display the image.

If you’re interested how the image source can be used by the attackers, check out this discussion.

Writing our policy

A rule in the content security policy consists of two parts:

  • a directive that represents the resource type the rule applies to,
  • the value that describes what content is valid for the given resource.

To better illustrate it, let’s provide a simple example:

For the keyword to be valid

We only allow images from the current origin thanks to using  and the keyword. Therefore, if we don’t host our website at, the browser cannot fetch the image located at This results in the following error:

Refused to load the image ‘’ because it violates the following Content Security Policy directive: “img-src ‘self'”.

Fortunately, we can provide multiple values for a given resource type.

Thanks to adding above, we allow images from both our origin and from

Specifying sources for JavaScript

Another essential directive is , where we specify valid sources for JavaScript.

By specifying , we allow JavaScript only from the current origin. While that increases safety, there are cases where this might be troublesome. For example, a typical case is to fetch jQuery from CDN.

Using would cause an error:

Refused to load the script ‘’ because it violates the following Content Security Policy directive: “script-src ‘self'”. Note that ‘script-src-elem’ was not explicitly set, so ‘script-src’ is used as a fallback.

To deal with it, we can explicitly allow JavaScript from

Dealing with inline JavaScript

Specifying disallows the browser from executing inline JavaScript too.

We might encounter this issue when using Google Tag Manager, for example. We could deal with this problem by using the keyword.

Doing the above is discouraged because it allows the browser to execute all inline code. Doing that would turn off one of the most significant advantages that Content Security Policy provides.

A good way of dealing with this issue is by providing a hash of our inline script. However, before doing that, we can run our website and investigate the error:

Refused to execute inline script because it violates the following Content Security Policy directive: “script-src ‘self'”. Either the ‘unsafe-inline’ keyword, a hash (‘sha256-YcgJt52q8mzxiLdUZrxAHMLSM7JxwURiNHAtn6WQFMU=’), or a nonce (‘nonce-…’) is required to enable inline execution.

Above, we can see that the browser generates the hash that we should use.

The above works great if our inline script does not change often. However, it would not work well with JavaScript that we generate dynamically.

We need to generate a nonce used both in the tag and in the Content-Security-Policy header to deal with such JavaScript. It should be different for each response. We can achieve it easily with the following libraries:



Applying multiple rules

So far, we’ve been applying one rule at a time. We can provide multiple rules separated with semicolons.

Although there are many directives besides and to choose from, one of them stands out. With , we can define a fallback for other directives.

For a complete list of available directives check out the documentation.

By defining , we configure a few things:

  • we determine the value for ,
  • we set up the value for every other fetch directive.

A common use case might be to allow images from any source while still blocking other dangerous content. For example, we can use with a wildcard to allow images from all subdomains of . We can also use just to allow images from any source.


In this article, we’ve gone through what cross-site scripting (XSS) is. We’ve also learned about content security policy (CSP) and how it can help us. This included learning about different resources we might want to block, such as images and javascript. We’ve emphasized inline javascript and allowed it with hashes and nonces. Examing various use cases also required us to apply multiple rules using wildcards and fallbacks. The content security policy can come in handy if we want an additional layer of protection again cross-site scripting.

Notify of
Inline Feedbacks
View all comments