What Is a Content-Security-Policy Header and How Do You Set One?
Content-Security-Policy is probably the most important security header you're not using. It tells the browser exactly where your page is allowed to load resources from, including scripts, styles, images, fonts, and iframes. Without it, any cross-site scripting (XSS) vulnerability in your app gives an attacker free rein to load and execute code from anywhere.
CSP is also the most intimidating header to configure. The syntax is unusual, the directives are numerous, and getting it wrong can break your site. This post breaks it down into something practical.
Why CSP matters
Without a Content-Security-Policy header, your browser will load and execute any script, style, or resource that your HTML references. If an attacker finds a way to inject HTML into your page (through an XSS vulnerability, a compromised third-party script, or a stored injection), they can load a script from their own server and it will execute with full access to your page, your cookies, and your users' sessions.
CSP doesn't prevent XSS vulnerabilities from existing in your code. What it does is limit the damage. Even if an attacker injects a <script src="https://evil.com/steal.js"> tag into your page, the browser will refuse to load it because evil.com isn't in your CSP allowlist.
Think of it as a seatbelt. It doesn't prevent crashes, but it dramatically reduces the harm when one happens.
The basic syntax
A CSP header is a series of directives separated by semicolons. Each directive controls a specific type of resource:
Content-Security-Policy: directive-1 value1 value2; directive-2 value3; directive-3 value4
The most important directives:
default-src is the fallback. If you don't specify a directive for a particular resource type, default-src applies. Start here.
script-src controls where JavaScript can be loaded from. This is the most security-critical directive.
style-src controls where CSS can be loaded from.
img-src controls where images can be loaded from.
font-src controls where fonts can be loaded from.
connect-src controls where your JavaScript can make network requests to (fetch, XMLHttpRequest, WebSocket).
frame-ancestors controls which sites can embed your page in an iframe. This replaces X-Frame-Options.
form-action controls where forms can submit to.
A practical starter policy
Here's a CSP that works for most modern web apps as a starting point:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'
Let's break that down:
default-src 'self' means by default, only load resources from your own domain.
script-src 'self' means only execute JavaScript files from your own domain. No inline scripts, no external CDNs unless you add them.
style-src 'self' 'unsafe-inline' allows your own stylesheets and inline styles. Most frameworks (including Tailwind, styled-components, and emotion) need 'unsafe-inline' for styles. This is an acceptable trade-off because inline styles are far less dangerous than inline scripts.
img-src 'self' data: https: allows images from your domain, data URIs (for inline images), and any HTTPS source. You might want to tighten this if you know exactly where your images come from.
font-src 'self' https://fonts.gstatic.com allows fonts from your domain and Google Fonts.
connect-src 'self' restricts API calls to your own domain. If you call external APIs, add them here.
frame-ancestors 'none' prevents any site from embedding yours in an iframe.
form-action 'self' prevents forms from being submitted to external domains.
base-uri 'self' prevents attackers from changing the base URL of your page.
Setting CSP in Next.js
In your next.config.js (or next.config.mjs):
const cspHeader = `
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self';
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
`.replace(/\n/g, ' ').trim();
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader,
},
],
},
];
},
};
Setting CSP in Express
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'"
);
next();
});
Setting CSP in Nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'" always;
Adding third-party services
The starter policy above will break your site if you use third-party scripts. Here's how to add common services:
Google Analytics / Google Tag Manager:
script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
connect-src 'self' https://www.google-analytics.com;
img-src 'self' data: https: https://www.google-analytics.com;
Stripe:
script-src 'self' https://js.stripe.com;
frame-src https://js.stripe.com;
Vercel Analytics:
script-src 'self' https://va.vercel-scripts.com;
connect-src 'self' https://vitals.vercel-insights.com;
Add each service's domains to the relevant directives. The service's documentation usually tells you what domains to allowlist, and if it doesn't, check the browser console for blocked resource errors after enabling CSP.
Testing with report-only mode
If you're worried about breaking your live site, start with Content-Security-Policy-Report-Only instead of Content-Security-Policy. This logs violations to the browser console without actually blocking anything.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; ...
Deploy this, open your site, and check the browser console. Every resource that would be blocked shows up as a violation. Add the necessary domains to your policy until no violations appear, then switch from Report-Only to the enforcing Content-Security-Policy header.
Common mistakes
Using 'unsafe-eval' in script-src. Some older libraries need eval() to work, and adding 'unsafe-eval' to your CSP allows it. This significantly weakens your policy because it lets attackers execute arbitrary code strings. If possible, find a library version that doesn't need eval.
Forgetting connect-src. Your CSP blocks script-src from loading external scripts, but if connect-src allows any domain, an attacker can still exfiltrate data via fetch requests. Keep connect-src as tight as your script-src.
Not setting frame-ancestors. Even with a strict CSP, if you don't include frame-ancestors 'none' (or a specific allowlist), your site can still be embedded in an iframe on a malicious site for clickjacking.
How to check yours
Open your browser dev tools, go to the Network tab, click on the main page request, and look for the Content-Security-Policy response header. If it's not there, you don't have one.
For a quick automated check, run your URL through Hexora at hexora.uk. It checks for CSP and six other security headers, showing you exactly what's missing and what values to set.
Start strict, loosen as needed
The safest approach is to start with the strictest policy you can (default-src 'self') and only add exceptions when something breaks. Every domain you add to your CSP is a domain you're trusting to not serve malicious content. Keep that list as short as possible.
Worried about your own site's security? Get a free scan in seconds.
Scan your site for free