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-ancestors directive 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, and base-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-requests or block-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:

  1. Start with report-only - deploy the strictest policy you think is correct using the report-only header. Monitor violations for at least a week.
  2. Analyze violations - identify legitimate resources being blocked (third-party analytics, CDN fonts, embedded widgets) and add them to your allowlist.
  3. Iterate - update the report-only policy and watch for new violations. Repeat until the violation reports are clean.
  4. Enforce - switch from Content-Security-Policy-Report-Only to Content-Security-Policy. Keep the report-uri directive 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:

  1. 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'.
  2. Overly broad wildcards. A policy like script-src * or script-src https: allows scripts from any HTTPS origin, including attacker-controlled sites. Be specific about which domains you trust.
  3. 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.
  4. 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.
  5. Setting CSP via <meta> tag for frame-ancestors. The frame-ancestors directive is ignored in meta tags - it only works as an HTTP header. The same applies to report-uri and sandbox.
  6. 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.

Frequently Asked Questions

A Content Security Policy (CSP) is an HTTP response header that tells the browser which sources of content (scripts, styles, images, fonts, etc.) are allowed to load on a page. It acts as an allowlist that blocks any resource not explicitly permitted, providing a strong defense against cross-site scripting (XSS), clickjacking, and other code injection attacks.
A nonce is a random, single-use token generated per request and added to both the CSP header and inline script tags. A hash is a cryptographic digest (SHA-256, SHA-384, or SHA-512) of the exact script content. Nonces are better for dynamic inline scripts because you only need to match the token. Hashes are better for static inline scripts because the content never changes, but any whitespace change invalidates the hash.
Use the Content-Security-Policy-Report-Only header instead of Content-Security-Policy. Report-only mode logs violations to the browser console and optionally to a reporting endpoint without actually blocking any resources. This lets you identify everything your policy would block before enforcing it in production.
'unsafe-inline' allows any inline script or style to execute, which effectively disables CSP's XSS protection. An attacker who can inject HTML into your page can include a script tag that runs arbitrary code. Use nonces or hashes for inline scripts instead of 'unsafe-inline' to maintain XSS protection while still allowing your own inline code.
CSP's frame-ancestors directive is the modern replacement for X-Frame-Options and is more flexible because it supports multiple origins and wildcards. However, you should still send X-Frame-Options alongside CSP for backward compatibility with older browsers. Similarly, CSP complements (but does not replace) headers like Strict-Transport-Security, X-Content-Type-Options, and Referrer-Policy.