Content Security Policy (CSP) is a browser security mechanism that lets website operators declare which sources of content are allowed to load and execute. Think of it as an allowlist that the browser enforces — if a script tries to load from a domain not in your CSP, the browser blocks it, regardless of whether the page's HTML referenced it.
CSP is your strongest defense against cross-site scripting (XSS). Even if an attacker injects a <script> tag into your page, a properly configured CSP prevents that script from executing because its source isn't in the allowlist. This is defense in depth — it doesn't replace input validation, but it dramatically reduces the impact of injection vulnerabilities.
CSP is delivered via the Content-Security-Policy HTTP response header. It can also be set via a <meta> tag, but the header is preferred because it covers all resources including those loaded before HTML parsing begins.
CSP policies are composed of directives, each controlling a specific resource type. Here are the directives you'll use most:
default-src — The fallback. If a specific resource type doesn't have its own directive, the browser uses this. Most policies start with default-src 'self' and then open up specific types as needed.script-src — Controls JavaScript sources. The most critical directive for XSS prevention. Includes inline scripts, external files, eval, and Web Workers.style-src — Controls CSS sources. Inline styles, external stylesheets, and <style> blocks.img-src — Controls image sources. Include CDN domains, data: URIs, and any third-party image hosts.font-src — Web fonts. Usually your domain plus Google Fonts or similar.connect-src — Controls where JavaScript can make network requests (fetch, XMLHttpRequest, WebSocket, EventSource). Critical for API endpoints.frame-src — Controls which pages can be embedded in iframes on your site.frame-ancestors — The reverse: controls who can embed YOUR site in an iframe. Replaces X-Frame-Options with more granularity.object-src — Controls Flash, Java, and other plugins. Set to 'none' unless you have a specific need.base-uri — Restricts the <base> tag, preventing injection of a malicious base URL that redirects relative URLs.form-action — Restricts where forms can submit. Prevents attackers from redirecting form submissions to their server.Each directive takes a space-separated list of source values:
'self' — Same origin (same scheme, host, port). This is your primary source.'none' — Block everything for this directive. Use for object-src when you have no plugins.'unsafe-inline' — Allow inline <script> and onclick= handlers. Defeats much of CSP's XSS protection. Avoid if possible.'unsafe-eval' — Allow eval(), new Function(), etc. Required by some older frameworks. Avoid in modern apps.https://cdn.example.com — Allow a specific origin. Can include paths (https://cdn.example.com/js/) but not wildcards in the path.*.example.com — Allow any subdomain. Note: * matches any subdomain but does NOT match the apex domain.data: — Allow data: URIs. Useful for inline images, risky for scripts.'nonce-' — Allow scripts/styles that carry a matching nonce attribute. Preferred approach for allowing specific inline scripts.'sha256-' — Allow a specific inline script whose content matches the hash. Useful for stable inline scripts.Start restrictive and open up as needed. Here's a progressive approach:
Step 1 — Observe: Deploy CSP in report-only mode first. The header Content-Security-Policy-Report-Only doesn't enforce anything — it just sends reports to a specified endpoint. This lets you see what would be blocked without breaking anything.
Content-Security-Policy-Report-Only:
default-src 'self';
report-to /csp-report
Step 2 — Baseline: Once you've collected reports for a few days, build your policy:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://pagead2.googlesyndication.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self';
frame-ancestors 'self';
object-src 'none';
base-uri 'self';
form-action 'self'
Step 3 — Tighten: Replace 'unsafe-inline' with nonces. Generate a random nonce per request and add it to allowed script/style tags:
// Server generates: nonce="r4nd0mstr1ng123"
Content-Security-Policy:
script-src 'self' 'nonce-r4nd0mstr1ng123';
<script nonce="r4nd0mstr1ng123">
// Your inline script — now allowed by CSP
</script>
The single biggest CSP weakness is 'unsafe-inline' in script-src. It allows any inline script to execute, which means an injected <script>alert(1)</script> still works. To get real XSS protection, you need to eliminate 'unsafe-inline'. Two approaches:
nonce="..." to every legitimate inline script. CSP allows only scripts with the matching nonce. Attackers can't predict the nonce, so injected scripts without it get blocked. This is the recommended approach for dynamic applications.'self' don't need nonces or hashes. Move as much inline JavaScript as possible to .js files.For style-src, 'unsafe-inline' is more commonly accepted because CSS injection is less dangerous than JavaScript injection (though not harmless). Many production sites keep style-src 'unsafe-inline' while tightening script-src.
CSP violations can be reported to your server for monitoring. Use the report-to or legacy report-uri directive:
Content-Security-Policy:
default-src 'self';
report-to csp-endpoint;
Report-To: {"group":"csp-endpoint","max_age":86400,
"endpoints":[{"url":"https://your-site.com/api/csp"}]}
The browser sends JSON reports containing the violated directive, the blocked URI, the line number, and the document URI. Aggregate these in a log or dashboard to catch policy gaps and detect injection attempts.
Free services like Report URI and Sentry can collect CSP reports without building your own endpoint. For small sites, this is the easiest path.
* in script-src: This effectively disables CSP for scripts. Any external script can load. Always list specific domains.base-uri: Without it, an attacker can inject a <base> tag that redirects all relative URLs (including script sources) to their server. Set base-uri 'self'.data: in script-src: Data URIs can encode arbitrary JavaScript. Keep data: restricted to img-src and font-src.frame-ancestors: Without it, any site can iframe yours, enabling clickjacking attacks. Set frame-ancestors 'self' unless you need specific embedders.