Applying Content Security Policy in Symfony to Reduce XSS Risks
Tags: php, symfony, security
Published: 2023-05-14 18:29:28

Protecting applications against XSS attacks is one of the most important things we can do to make them more secure. In this post, I'm going to show you how to configure Content Security Policy in Symfony to reduce the risk of XSS attacks.

# What is XSS

Cross-Site Scripting (XSS) attacks are one of the most common attacks on web applications. They involve injecting JavaScript code into a page that can be executed in the user's browser. This way, an attacker can take control of the user's session, steal data or perform other undesirable actions. 

# What is Content Security Policy

There are many ways to defend against these types of attacks. One of them is the use of the Content-Security-Policy (CSP) header. This header, sent in response to a browser request, specifies what resources (scripts, styles, fonts, etc.) can be loaded on our website. Thanks to this, even if a hacker manages to inject malicious code into our page, a browser that supports CSP will effectively block its execution, reducing the risk of attack.

# Different approaches of applying CSP

If we want to apply this method of securing our application, we can do it ourselves by adding a header to the response or use a popular library, NelmioSecurityBundle, which has many other security options. In this post, I'm going to focus on the second method.

# Before we start

I've prepared an example HTML code to illustrate how CSP will affect our website. Let's take a quick look at it.

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        button {
            color: red !important;
        }
    </style>
</head>
<body>
    <button class="btn btn-primary" style="font-weight: bold;">Submit</button>
    
    <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"></script>
    <script>
        $(document).ready(function () {
            $('button').text('Click me!');
        });
    </script>
</body>
</html>
```

It involves loading a few external resources like styles and scripts for Bootstrap and jQuery, as well as using inline styles and scripts. In the next steps, I'm going to show you how to set up CSP and how it can impact our website.

I'll be using Chrome console to display any violation of CSP.

Without any CSP policy configured, the console should not report any violations and the button from above HTML should look like this after all styles and scripts are loaded and executed:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ssuj3vd2zab1izwtgt0n.png)

# Configuring Content Security Policy

To begin using CSP, we need to add a few lines to `config/packages/nelmio_security.yaml` file. I'm using version `3.0.0` of NelmioSecurityBundle. We'll start with the most restrictive configuration.

```yaml
nelmio_security:
  csp:
    enforce:
      report-uri: '%router.request_context.base_url%/nelmio/csp/report'
      default-src:
        - 'none'
      script-src:
        - 'self'
```

The above code configures CSP with `enforce` mode, which blocks all resources except for those that are defined on the lists.


- `report-uri` defines a URI where the browser should send a report if it detects a violation of CSP. 
- `default-src` specifies the default or fallback resources allowed to be loaded on the page. In our case, we don't want to allow any undefined sources to be loaded, so we set it to `'none'`.
- `script-src` defines a list of script resources that are allowed to be loaded and executed. Let's use `'self'` to only allow scripts from website's origin.

With CSP configured like this, the browser should block all resources that are not allowed. Let's check it.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7zsvxwh2hgoa7xfm9r1p.png)

The browser has blocked all resources that are not listed. It also blocked scripts and styles that are defined in the HTML file. According to this, the styles from Bootstrap were not applied to the button. Our styles did not change the colour of the text and did not make it bold. And our script did not change the text on the button. This is because we did not define them on `script-src` and `style-src` lists.

The console displayed the following error messages:

```
Refused to load the stylesheet 'https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css' because it violates the following Content Security Policy directive: "default-src 'none'". Note that 'style-src-elem' was not explicitly set, so 'default-src' is used as a fallback.

Refused to apply inline style because it violates the following Content Security Policy directive: "default-src 'none'". Either the 'unsafe-inline' keyword, a hash ('sha256-So78BYT2mbjtQqZqHbPQDdRiZpvjnGBwZCYxIdxMMOE='), or a nonce ('nonce-...') is required to enable inline execution. Note also that 'style-src' was not explicitly set, so 'default-src' is used as a fallback.

Refused to apply inline style because it violates the following Content Security Policy directive: "default-src 'none'". Either the 'unsafe-inline' keyword, a hash ('sha256-+YWRMZ88jMyO7jVlBA52tZADiPobPIUA8LAWee68Fvs='), or a nonce ('nonce-...') is required to enable inline execution. Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present. Note also that 'style-src' was not explicitly set, so 'default-src' is used as a fallback.

Refused to load the script 'https://code.jquery.com/jquery-3.6.4.min.js' 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.

Refused to load the script 'https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js' 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.

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-aAnMJGUIj4IqwyFSfCsQ989Go5ey5e7X+YACSD316t8='), or a nonce ('nonce-...') is required to enable inline execution.
```

The browser also tried to send the violations to the URL we defined in `report-uri`. They contained information about what resources had been blocked. Below is a sample report:

```json
{
  "csp-report": {
    "document-uri": "http://localhost/content-security-policy",
    "referrer": "",
    "violated-directive": "script-src-elem",
    "effective-directive": "script-src-elem",
    "original-policy": "default-src \u0027none\u0027; script-src \u0027self\u0027; report-uri /nelmio/csp/report",
    "disposition": "enforce",
    "blocked-uri": "inline",
    "line-number": 18,
    "source-file": "http://localhost/content-security-policy",
    "status-code": 200,
    "script-sample": ""
  }
}
```

# Defining a list of allowed resources

To restore our website's functionality, we need to define a list of resources that can be fetched and executed.

```yaml
nelmio_security:
    csp:
        enforce:
            report-uri: '%router.request_context.base_url%/nelmio/csp/report'
            default-src:
                - 'none'
            script-src:
                - 'self'
                - 'unsafe-inline'
                - 'code.jquery.com'
                - 'cdn.jsdelivr.net'
            style-src:
                - 'cdn.jsdelivr.net'
                - 'unsafe-inline'
```

After refreshing the page, we can see that the button is styled again, and the text has changed. The console isn't showing any errors, so everything seems to be working as expected.
What's more, we can see a `Content-Security-Policy` header in the response:

```
Content-Security-Policy: default-src 'none'; script-src 'self' 'unsafe-inline' code.jquery.com cdn.jsdelivr.net; style-src cdn.jsdelivr.net 'unsafe-inline'; report-uri /nelmio/csp/report
```

# Dealing with unsafe inline and unsafe eval

As you might have noticed, we added `unsafe-inline` value to the style and script lists. This is not a good practice because if someone manages to inject malicious code, it will be executed, effectively disabling the XSS protection mechanism of CSP.

But what if we need to use inline scripts or styles? We can still do it, but we have to use `nonce` or `hash` values.

What is `nonce`? It's a random string that is generated for each request. It is used to allow usage of inline scripts and styles. The same `nonce` value has to be applied to script or style tag and to the `Content-Security-Policy` header.

In our case, with Nelmio Security Bundle and Twig, we can use the `csp_nonce` function in Twig to generate a nonce. 

After using `csp_nonce` function for either script or style, `nonce` will be generated and automatically applied to the `Content-Security-Policy` header.

Then, we will be able to remove `unsafe-inline` value from `script-src` and `style-src` lists. In fact, those values don't work along with `nonce`, which means that if we added nonce to CSP list, `unsafe-inline` will be ignored.

We have to remember to add `nonce` to each script and style tag. Otherwise, they will not be executed.

You may ask what about inline styles? It's not possible to generate nonce for them, so they will be blocked. A workaround for this case is to move inline styles to external files or to style tag with `nonce` attribute.

In practice, updated Twig template will look like this:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style nonce="{{ csp_nonce('style') }}">
        button {
            color: red !important;
            font-weight: bold !important;
        }
    </style>
</head>
<body>
    <button class="btn btn-primary">Submit</button>
    <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"></script>
    <script nonce="{{ csp_nonce('script') }}">
        $(document).ready(function () {
            $('button').text('Click me!');
        });
    </script>
</body>
</html>
```

Content-Security-Policy header will now contain nonce values for styles and scripts:

```
Content-Security-Policy: default-src 'none'; script-src 'self' 'unsafe-inline' code.jquery.com cdn.jsdelivr.net 'nonce-G41nkEkLQJ77SLEx0j3cXA=='; style-src cdn.jsdelivr.net 'unsafe-inline' 'nonce-G41nkEkLQJ77SLEx0j3cXA=='; report-uri /nelmio/csp/report
```

# Report-Only mode

If we want to implement Content-Security-Policy protection into an existing, large application, we may not be able to list all the resources. In such a case, before we turn on enforce mode, we can use report-only mode. In this mode, the CSP header will report all violations, but it won't block any resource. This allows us to see what resources are used on the site and which ones we should add to the allowed list. To enable report-only mode, we need to change the `enforce` value to `report` in the configuration file:

```yaml
nelmio_security:
  csp:
    report:
      report-uri: '%router.request_context.base_url%/nelmio/csp/report'
      default-src:
        - 'none'
      script-src:
        - 'self'
      style-src:
        - 'self'
```

The console will then display errors about CSP violations, but no resource will be blocked.

```
[Report Only] Refused to load the stylesheet 'https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css' because it violates the following Content Security Policy directive: "style-src 'self' 'unsafe-inline' 'nonce-jbOYi9qK+tahki7w9Yw7Cw=='". Note that 'style-src-elem' was not explicitly set, so 'style-src' is used as a fallback.

[Report Only] Refused to load the script 'https://code.jquery.com/jquery-3.6.4.min.js' because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-inline' 'nonce-jbOYi9qK+tahki7w9Yw7Cw=='". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

[Report Only] Refused to load the script 'https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js' because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-inline' 'nonce-jbOYi9qK+tahki7w9Yw7Cw=='". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.
```

It's also worth noting that the `Content-Security-Policy` header has changed to `Content-Security-Policy-Report-Only`:

```
Content-Security-Policy-Report-Only: default-src 'none'; script-src 'self' 'unsafe-inline' 'nonce-jbOYi9qK+tahki7w9Yw7Cw=='; style-src 'self' 'unsafe-inline' 'nonce-jbOYi9qK+tahki7w9Yw7Cw=='; report-uri /nelmio/csp/report
```

An important thing to mention is that we can use both enforce and report-only modes at the same time. In that case, all resources not listed on the `Content-Security-Policy` list will be blocked, and all resources not listed on `Content-Security-Policy-Report-Only` will be reported. 

For large applications, it's worth starting with report-only mode and configuring the URL to report violations under the `report-uri` key. It can be an internal endpoint in our application. We can also use dedicated portals for this purpose, e.g. [report-uri.com](https://report-uri.com/). Then, after collecting and listing all resources, we can switch to enforce mode.

# More info

Managing scripts and styles is just an example. There are many more options that can be used to secure our application. We can manage the list of allowed resources for images, fonts, frames, and more. You can find more information about the Content-Security-Policy header in the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). It's also worth visiting the [Nelmio Security Bundle documentation](https://symfony.com/bundles/NelmioSecurityBundle/current/index.html#content-security-policy) to learn more about it.

# Summary

This post aimed to introduce the basic concepts related to Content-Security-Policy and show how to implement this policy in Symfony-based application. Implementing it will further secure our site against XSS attacks, as well as the loading of external resources. However, we must remember that this is not the only way of securing our application. It is also worth remembering other security measures, such as validating form fields to ensure they do not contain malicious content and displaying content in templates in an appropriate manner.