Content Security Policy (CSP) Guide: Protect Your Website from XSS
What Is Content Security Policy?
Content Security Policy (CSP) is an HTTP response header that tells the browser which sources of content are allowed to load on your web page. It is one of the most effective defenses against Cross-Site Scripting (XSS) attacks — the most common web vulnerability.
Without CSP, a browser will execute any JavaScript, load any image, and connect to any server that your HTML references. If an attacker injects a malicious script into your page (through a stored XSS vulnerability, a compromised third-party library, or user-generated content), the browser executes it without question.
With CSP, you define a whitelist of trusted sources. The browser blocks anything that is not on the list.
How XSS Attacks Work
Understanding the threat helps you understand why CSP matters.
Stored XSS
An attacker submits malicious JavaScript through a form (comment, profile, review). The script is stored in your database and served to every user who views that content. The script runs in the context of your domain, giving it access to cookies, session tokens, and user data.
Reflected XSS
The attacker crafts a URL containing malicious JavaScript in a query parameter. When a victim clicks the link, the server reflects the malicious input back in the page, and the browser executes it.
Third-Party Compromise
A legitimate third-party script (analytics, ads, chat widgets) gets compromised. Because your page loaded it willingly, the browser trusts it completely. The compromised script can steal user data, redirect users, or inject additional malware.
CSP mitigates all three scenarios by restricting what scripts can execute and where they can send data.
CSP Basics
A CSP is set as an HTTP response header:
Content-Security-Policy: directive1 value1; directive2 value2;
Key Directives
| Directive | Controls | Example |
|---|---|---|
default-src | Fallback for all resource types | 'self' |
script-src | JavaScript sources | 'self' https://cdn.example.com |
style-src | CSS sources | 'self' 'unsafe-inline' |
img-src | Image sources | 'self' data: https: |
connect-src | AJAX, WebSocket, fetch destinations | 'self' https://api.example.com |
font-src | Font file sources | 'self' https://fonts.gstatic.com |
frame-src | iframe sources | 'none' |
object-src | Flash, Java, plugins | 'none' |
base-uri | Restricts <base> element URLs | 'self' |
form-action | Form submission destinations | 'self' |
Source Values
| Value | Meaning |
|---|---|
'self' | Same origin only |
'none' | Block everything |
https://example.com | Specific domain |
https: | Any HTTPS source |
data: | Data URIs (inline images, fonts) |
'unsafe-inline' | Allow inline scripts/styles (weakens CSP significantly) |
'unsafe-eval' | Allow eval() and similar (weakens CSP significantly) |
'nonce-abc123' | Allow specific inline scripts with matching nonce |
'strict-dynamic' | Trust scripts loaded by already-trusted scripts |
Building Your CSP Step by Step
Step 1: Start With Report-Only Mode
Before enforcing a CSP, deploy it in report-only mode to see what would be blocked without actually breaking anything:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports
This logs violations to your specified endpoint without blocking them. Monitor for a few days to see what your site loads and from where.
Step 2: Set a Strict Default
Start with a restrictive default and then add exceptions:
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; font-src 'self'; base-uri 'self'; form-action 'self';
This blocks everything by default and only allows resources from your own domain.
Step 3: Add Third-Party Sources
Add specific domains for each third-party service you use:
Content-Security-Policy:
default-src 'none';
script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
style-src 'self' https://fonts.googleapis.com;
img-src 'self' https://www.google-analytics.com data:;
connect-src 'self' https://www.google-analytics.com;
font-src 'self' https://fonts.gstatic.com;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
Step 4: Handle Inline Scripts With Nonces
Inline scripts (<script>alert('hi')</script>) are blocked by a strict CSP. Instead of using 'unsafe-inline' (which defeats the purpose of CSP), use nonces:
<!-- Server generates a unique nonce for each request -->
<script nonce="a1b2c3d4e5">
// This script is allowed because the nonce matches the CSP
console.log("Hello");
</script>
Content-Security-Policy: script-src 'nonce-a1b2c3d4e5' 'strict-dynamic';
The nonce must be unique per request and cryptographically random. An attacker cannot predict the nonce, so injected scripts without the correct nonce are blocked.
Step 5: Enforce
Once your report-only CSP runs clean for a few days, switch from Content-Security-Policy-Report-Only to Content-Security-Policy. Keep the report-uri directive to catch any violations you missed.
CSP for Common Frameworks
Next.js
In next.config.js:
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
}
];
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
}
};
Note: Next.js requires 'unsafe-eval' in development mode. Use nonces in production for a stricter policy.
Nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self';" always;
Apache
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self';"
Meta Tag (Fallback)
If you cannot set HTTP headers, use a meta tag (with limitations — report-uri and frame-ancestors do not work in meta tags):
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self';">
Common CSP Mistakes
Using ‘unsafe-inline’ for Scripts
This allows any inline script to execute, including injected ones. Use nonces or hashes instead.
Overly Broad Whitelists
script-src https: allows any HTTPS source to serve JavaScript. An attacker can host a malicious script on any HTTPS server. Be specific about which domains you trust.
Forgetting connect-src
If you restrict script-src but leave connect-src open, a compromised script can still send stolen data to any server via fetch() or XMLHttpRequest.
Not Setting object-src
Always set object-src 'none' unless you specifically need Flash or Java plugins (you almost certainly do not). Plugin-based attacks bypass other CSP protections.
Ignoring frame-ancestors
frame-ancestors controls who can embed your page in an iframe. Set it to 'none' or 'self' to prevent clickjacking attacks.
Testing Your CSP
- Browser DevTools: Open the Console tab. CSP violations appear as errors with detailed messages about what was blocked and why.
- Report-Only mode: Deploy your CSP in report-only mode first and collect violation reports.
- Online validators: CSP evaluation tools can check your header for common mistakes.
For related security improvements, generate strong passwords with the Password Generator and check your security headers alongside CSP implementation.
Conclusion
Content Security Policy is one of the strongest defenses against XSS attacks. Start in report-only mode, build a strict policy with specific source whitelists, use nonces for inline scripts, and enforce it once you have verified it does not break your site.
The effort to implement CSP is modest compared to the protection it provides. Combined with other security headers and tools like the Hash Generator for integrity checks, CSP forms a critical layer in your web application’s defense.