DEV Community

Jarosław Szutkowski
Jarosław Szutkowski

Posted on

Applying Content Security Policy in Symfony to Reduce XSS Risks

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.

<!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>
Enter fullscreen mode Exit fullscreen mode

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

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.

nelmio_security:
  csp:
    enforce:
      report-uri: '%router.request_context.base_url%/nelmio/csp/report'
      default-src:
        - 'none'
      script-src:
        - 'self'
Enter fullscreen mode Exit fullscreen mode

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

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.
Enter fullscreen mode Exit fullscreen mode

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:

{
  "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": ""
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

<!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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

nelmio_security:
  csp:
    report:
      report-uri: '%router.request_context.base_url%/nelmio/csp/report'
      default-src:
        - 'none'
      script-src:
        - 'self'
      style-src:
        - 'self'
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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. 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. It's also worth visiting the Nelmio Security Bundle documentation 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.

Top comments (0)