Content Security Policy Explained: A Developer's Guide to CSP Headers
Cross-site scripting (XSS) remains one of the most common and dangerous web vulnerabilities. An attacker who can inject a script into your page can steal session tokens, redirect users, deface content, or exfiltrate form data. Content Security Policy (CSP) is the browser-level defense mechanism that stops these attacks at the source - by telling the browser exactly which resources are allowed to load and execute on your pages.
This guide covers everything you need to deploy CSP effectively: what it prevents, how directives work, the nonce-vs-hash decision, report-only mode for safe rollouts, and the mistakes that silently break your policy. If you need to generate a CSP header right now, try our CSP Header Generator - it builds a valid policy interactively and exports it as an HTTP header or meta tag.
What Does CSP Prevent?
CSP is designed to mitigate several classes of attacks that rely on injecting unauthorized content into a web page:
- Cross-site scripting (XSS) - the primary threat CSP addresses. By restricting which scripts can execute, CSP blocks injected inline scripts and scripts loaded from attacker-controlled domains, even if the attacker finds an injection point in your HTML.
- Clickjacking - the
frame-ancestorsdirective controls which sites can embed your page in an iframe, preventing UI redress attacks where an invisible frame tricks users into clicking unintended elements. - Data injection and exfiltration - by restricting
connect-src,form-action, andbase-uri, CSP can prevent injected code from sending stolen data to an attacker's server or redirecting form submissions. - Mixed content - CSP can enforce HTTPS for all subresources via
upgrade-insecure-requestsorblock-all-mixed-content, preventing protocol downgrade attacks. - Unauthorized resource loading - even without an active attack, CSP limits the blast radius of a compromised third-party script by restricting what other resources it can load.
CSP Directive Reference
A CSP policy is a semicolon-separated list of directives. Each directive controls a specific type of resource. Here are the directives you will use most often:
default-src
The fallback for any directive not explicitly set. If you only specify default-src 'self', all resource types (scripts, styles, images, fonts, connections) are restricted to your own origin. Always set this as your baseline and then open up specific directives as needed.
Content-Security-Policy: default-src 'self'
script-src
Controls which scripts can execute. This is the most security-critical directive. Common values:
# Allow scripts from your origin and a CDN
script-src 'self' https://cdn.example.com;
# Allow scripts with a specific nonce
script-src 'nonce-abc123def456';
# Allow scripts matching a hash (SHA-256)
script-src 'sha256-abcdef1234567890...';
Avoid 'unsafe-inline' and 'unsafe-eval' whenever possible - they undermine the XSS protection that CSP provides.
style-src
Controls which stylesheets can be applied. Similar source expressions as script-src. Inline styles require 'unsafe-inline', a nonce, or a hash. Note that many CSS-in-JS libraries inject inline styles, which complicates CSP deployment.
img-src
Controls which image sources are allowed. You often need to allow data: for inline images and specific CDN domains for user-uploaded content or third-party images:
img-src 'self' data: https://images.example.com https://cdn.example.com;
connect-src
Controls which URLs your JavaScript can connect to via fetch(), XMLHttpRequest, WebSocket, and EventSource. This is critical for preventing data exfiltration - even if an attacker injects a script, it cannot phone home if connect-src blocks the destination.
font-src, media-src, object-src, frame-src
These control fonts, audio/video, plugins (Flash/Java), and iframes respectively. Set object-src 'none' unless you specifically need plugins - this blocks a significant attack surface with zero cost to modern web applications.
frame-ancestors
Controls which pages can embed yours in an iframe. Unlike other directives, frame-ancestors is only valid in the HTTP header (not in a <meta> tag). It replaces the older X-Frame-Options header:
# Only allow your own site to frame your pages
frame-ancestors 'self';
# Block all framing
frame-ancestors 'none';
base-uri and form-action
base-uri restricts the URLs that can appear in a <base> tag, preventing attackers from changing the base URL to hijack relative links. form-action restricts where forms can submit data, blocking form hijacking attacks.
Nonces vs Hashes: Allowing Inline Scripts Safely
The safest CSP blocks all inline scripts. But real applications often need inline scripts for analytics snippets, framework hydration, or configuration. CSP provides two mechanisms to allow specific inline scripts without opening the floodgates.
Nonces
A nonce (number used once) is a random, base64-encoded string generated on every HTTP response. You include the nonce in both the CSP header and the script tag:
# HTTP Header
Content-Security-Policy: script-src 'nonce-4AEemGb0xJptoIGFP3Nd'
<!-- HTML -->
<script nonce="4AEemGb0xJptoIGFP3Nd">
console.log('This script is allowed');
</script>
The nonce must be cryptographically random and regenerated for every response. If you serve pages from a cache or CDN and cannot generate unique nonces per request, use hashes instead.
Hashes
A hash is a cryptographic digest of the exact script content. The browser computes the hash of each inline script and compares it against the allowed hashes in the CSP header:
# Generate the hash of your inline script
echo -n 'console.log("hello")' | openssl dgst -sha256 -binary | base64
# Result: RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=
# HTTP Header
Content-Security-Policy: script-src 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc='
Hashes are fragile - any change to the script content, including whitespace, invalidates the hash. They work best for scripts that never change, like analytics snippets or static configuration blocks.
When to Use Each
- Nonces - best for server-rendered pages where you control each response. Great for dynamic inline scripts whose content may vary.
- Hashes - best for static sites, CDN-served pages, and scripts whose content is fixed at build time.
Report-Only Mode: Safe CSP Rollout
Deploying CSP to production without testing will break your site. The Content-Security-Policy-Report-Only header is your safety net. It evaluates the policy and logs violations but does not block anything:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report
The recommended rollout strategy:
- Start with report-only - deploy the strictest policy you think is correct using the report-only header. Monitor violations for at least a week.
- Analyze violations - identify legitimate resources being blocked (third-party analytics, CDN fonts, embedded widgets) and add them to your allowlist.
- Iterate - update the report-only policy and watch for new violations. Repeat until the violation reports are clean.
- Enforce - switch from
Content-Security-Policy-Report-OnlytoContent-Security-Policy. Keep thereport-uridirective active so you catch any regressions.
Use the Reporting API v1 (report-uri) or v2 (report-to) to collect violation reports server-side. Services like report-uri.com and Sentry can aggregate and visualize CSP reports.
Common CSP Mistakes
Even experienced teams make these errors when deploying CSP:
- Using 'unsafe-inline' and 'unsafe-eval' as quick fixes. These disable the core XSS protection CSP provides. If you need inline scripts, use nonces or hashes. If a library requires
eval(), consider replacing it or using'strict-dynamic'. - Overly broad wildcards. A policy like
script-src *orscript-src https:allows scripts from any HTTPS origin, including attacker-controlled sites. Be specific about which domains you trust. - Forgetting base-uri and form-action. These directives do not fall back to
default-src. Without explicit restrictions, attackers can inject<base>or<form>tags to hijack navigation and form submissions. - Not accounting for third-party scripts. Ad networks, analytics, A/B testing tools, and chat widgets all load scripts from their own domains and often inject further scripts dynamically. Audit every third-party dependency before finalizing your CSP.
- Setting CSP via <meta> tag for frame-ancestors. The
frame-ancestorsdirective is ignored in meta tags - it only works as an HTTP header. The same applies toreport-uriandsandbox. - Static nonces. If your nonce is the same on every page load (hardcoded or cached), it provides zero security. Nonces must be random and unique per response.
A Practical CSP Deployment Strategy
Here is a step-by-step approach for adding CSP to an existing application:
# Step 1: Start strict, report only
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
report-uri /csp-violations
# Step 2: Add trusted third-party origins as needed
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://images.example.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
report-uri /csp-violations
# Step 3: Enforce (change header name)
Content-Security-Policy: ... (same policy as step 2)
CSP and Subresource Integrity (SRI)
CSP and SRI are complementary defenses. CSP controls which origins can serve resources; SRI verifies that the content of a specific resource has not been tampered with. Use them together for defense in depth:
<script src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
Generate SRI hashes with our SRI Hash Generator to ensure third-party scripts have not been modified.
Generate Your CSP Header
Building a correct CSP by hand is tedious and error-prone. Use our tools to generate and validate your security headers:
- CSP Header Generator - build a Content Security Policy interactively with directive autocomplete and violation preview.
- CORS Header Builder - configure Cross-Origin Resource Sharing headers alongside your CSP.
- SRI Hash Generator - generate Subresource Integrity hashes for your external scripts and stylesheets.