The most surprising thing about Content Security Policy (CSP) is that its primary strength isn’t preventing XSS, but rather significantly reducing the attack surface for any client-side code injection, even if that injection isn’t a classic XSS.
Let’s see CSP in action. Imagine a simple HTML page served from your CDN:
<!DOCTYPE html>
<html>
<head>
<title>My Awesome App</title>
<link rel="stylesheet" href="https://cdn.example.com/assets/style.css">
<script src="https://cdn.example.com/assets/app.js"></script>
</head>
<body>
<h1>Hello, World!</h1>
<img src="https://cdn.example.com/images/logo.png">
</body>
</html>
Without any CSP, a malicious actor could potentially inject arbitrary JavaScript or even load external resources if they find a way to manipulate the content. Now, let’s add a CSP header at the CDN edge. This is typically done via your CDN’s configuration interface. For Cloudflare, it might look something like this in their "Page Rules" or "Transform Rules":
Rule:
- URL:
*example.com/* - Setting: "Add Header"
- Header Name:
Content-Security-Policy - Header Value:
default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' https://cdn.example.com; img-src 'self' https://cdn.example.com; connect-src 'self' https://api.example.com;
This header tells the browser:
default-src 'self': By default, only allow resources from the same origin (example.com).script-src 'self' https://cdn.example.com: Allow scripts from the same origin and specifically fromcdn.example.com.style-src 'self' https://cdn.example.com: Allow stylesheets from the same origin andcdn.example.com.img-src 'self' https://cdn.example.com: Allow images from the same origin andcdn.example.com.connect-src 'self' https://api.example.com: Allow connections (like AJAX requests) to the same origin andapi.example.com.
If an attacker managed to inject <script>alert('XSS')</script> into the HTML, the browser would not execute it because the script-src directive doesn’t allow inline scripts. If they tried to load an image from http://evil.com/malicious.jpg, it would fail because img-src only permits self and https://cdn.example.com.
The problem CSP solves is preventing unauthorized code execution and data exfiltration. It acts as a whitelist for all client-side resources and origins your application is allowed to interact with. By enforcing this at the CDN edge, you ensure that all requests for your assets, regardless of where they originate from (your origin server, a misconfigured cache, or even a compromised intermediary), are served with the correct security policy. This is crucial because if your origin server is compromised, an attacker could potentially remove or alter the CSP header, negating its protection. The CDN’s enforcement acts as a strong, independent layer of defense.
The core mechanism is the browser’s interpretation of these directives. When the browser receives an HTML document with a CSP header, it parses the header and applies the rules to every subsequent resource request made by that page. If a resource request violates the policy (e.g., trying to load a script from an unlisted domain), the browser simply blocks it. This happens before the resource is even downloaded or potentially executed.
Here’s where it gets interesting: CSP’s report-uri or report-to directive is often overlooked but is critical for understanding what would have happened. When a violation occurs, the browser sends a JSON report to the specified URI. This isn’t just for debugging; it’s an active threat intelligence feed. For example, if you see reports coming in for script-src violations originating from http://malicious-domain.com, it’s a strong indicator that an injection attempt is occurring, even if your current policy successfully blocks it.
{
"csp-report": {
"document-uri": "https://example.com/page",
"referrer": "",
"violated-directive": "script-src",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self' https://cdn.example.com",
"blocked-uri": "http://malicious-domain.com/exploit.js",
"status-code": 200,
"source-file": "",
"line-number": 0,
"column-number": 0,
"script-sample": ""
}
}
The next step is understanding how to implement trusted types to further restrict DOM manipulation and protect against DOM-based XSS.