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>
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:
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'
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.
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:
{
"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.
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:
<!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:
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. 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)