RestingOwl owl logo RestingOwl

Content Security Policy (CSP):A Practical Guide to Stopping XSS

Quick Answer: A Content Security Policy (CSP) is an HTTP response header that tells the browser which sources of scripts, styles, images, and other resources are allowed to load. Its main job is to reduce the impact of cross-site scripting: even if an attacker injects a script, a strict CSP stops the browser from executing it. CSP is a mitigation layer, not a replacement for output encoding. The strongest policies block inline scripts and use per-request nonces or hashes to allow only trusted code.

Output encoding is the primary defense against cross-site scripting, but no large application encodes every output perfectly forever. Content Security Policy is the safety net for the encoding point you missed. It moves the decision of what code may run out of your templates and into an explicit policy the browser enforces on every page. This guide explains how CSP works, the directives that matter, how to build a policy that actually stops XSS, and the mistakes that quietly make a policy useless. For the attack it defends against, start with What Is Cross-Site Scripting (XSS)?

What Is a Content Security Policy?

A Content Security Policy is a set of rules, delivered in the Content-Security-Policy HTTP response header, that controls which resources a page is allowed to load and execute. Instead of trusting any script the page happens to contain, the browser checks each resource against your policy and blocks anything that does not match. The policy is a list of directives, each naming a resource type and the sources permitted for it.

Content-Security-Policy: default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self';
  img-src 'self' data:;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none'

This policy says: load everything from the site's own origin by default, allow scripts only from the origin and one named CDN, block all plugins (object-src 'none'), and forbid the page from being framed (frame-ancestors 'none'). An injected <script> pointing at an attacker's domain would not match script-src, so the browser refuses to run it.

How Does CSP Stop XSS?

Cross-site scripting works by getting attacker-controlled JavaScript to execute in the victim's browser. That script usually arrives in one of two forms: an inline <script> block injected into the HTML, or a <script src> that loads code from an attacker's server. A well-built CSP blocks both.

  • Blocking inline scripts: by default, a CSP with a script-src directive refuses to execute any inline script or event handler unless it carries a matching nonce or hash. An injected <script>alert(1)</script> has no valid nonce, so it never runs.
  • Restricting script sources: script-src 'self' means the browser will only load scripts from your own origin. An injected <script src="https://evil.tld/x.js"> is blocked because the attacker's domain is not on the allowlist.
  • Disabling dangerous evaluation: without 'unsafe-eval', calls to eval() and similar string-to-code APIs are blocked, closing a common DOM-based XSS sink.

The result is a strong second line of defense: to succeed, an attacker now needs both an injection point and a way to satisfy your policy. That is a far higher bar than injection alone. For how those injection points arise in the first place, see Types of XSS: Stored, Reflected and DOM-Based.

What Are the Most Important CSP Directives?

DirectiveWhat It ControlsRecommended Value
default-srcFallback for resource types you do not set explicitly'self'
script-srcWhere JavaScript may load and execute from'self' plus a nonce or hash; avoid 'unsafe-inline'
style-srcWhere CSS may load from'self' (or a nonce for inline styles)
img-srcWhere images may load from'self' data: as needed
object-srcPlugins such as Flash and Java applets'none'
base-uriWhat the page's tag may be set to'self'
frame-ancestorsWhich sites may embed this page in a frame'none' or your own origin
connect-srcWhere fetch, XHR, and WebSocket may connect'self' plus trusted APIs

object-src 'none' and base-uri 'self' are cheap wins that close injection tricks many policies forget. frame-ancestors 'none' also gives you clickjacking protection, replacing the older X-Frame-Options header.

How Do You Build a Strict CSP With Nonces?

The weakest common policies allow 'unsafe-inline', which permits any inline script and effectively disables CSP's XSS protection. The modern, strong approach is a nonce-based policy: the server generates a fresh random value on every response, adds it to the CSP header, and puts the same value on each legitimate inline script. Injected scripts cannot guess the nonce, so they are blocked.

// Server: fresh nonce per request
const nonce = crypto.randomBytes(16).toString('base64');
res.setHeader('Content-Security-Policy',
  `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'self'`);

// Template: only scripts with the matching nonce run
// <script nonce="${nonce}">...trusted code...</script>

Adding 'strict-dynamic' lets a trusted, nonced script load the additional scripts it needs without you allowlisting every CDN by hand, which keeps the policy short and maintainable. This is the pattern OWASP recommends for new applications.

What Is CSP Report-Only Mode?

Rolling out a strict CSP on a live site risks breaking legitimate scripts. The Content-Security-Policy-Report-Only header solves this: the browser enforces nothing but reports every violation it would have blocked to an endpoint you specify. You deploy the policy in report-only mode, watch the reports, fix the legitimate resources that would break, and only switch to the enforcing header once the violation stream is clean.

Content-Security-Policy-Report-Only: default-src 'self';
  script-src 'self'; report-uri /csp-violation-report

What Are the Most Common CSP Mistakes?

  • Allowing 'unsafe-inline' in script-src. This single value re-permits injected inline scripts and is the most common reason a CSP provides no real XSS protection. Use nonces or hashes instead.
  • Allowing 'unsafe-eval'. It keeps eval() and Function() working, which are DOM-based XSS sinks. Remove it unless a dependency genuinely requires it, then isolate that dependency.
  • Overly broad allowlists. Whitelisting a large CDN or a wildcard host can let an attacker host a script on that same trusted origin. Prefer nonces with 'strict-dynamic' over long host allowlists.
  • Forgetting object-src and base-uri. Leaving these unset can allow plugin-based or base-tag injection that bypasses your script-src.
  • Setting CSP once and never reviewing it. New features and third-party scripts change what the page loads. Keep report-only monitoring on so regressions surface.

Is CSP a Replacement for Output Encoding?

No. CSP is a mitigation, not a fix. Output encoding prevents the injection from being interpreted as HTML or script in the first place; CSP reduces the damage when an injection slips through. OWASP is explicit that CSP should be treated as defense in depth layered on top of encoding, never as a substitute for it. A page that encodes correctly and ships a strict CSP forces an attacker to defeat two independent controls. For the full set of controls, see the OWASP XSS prevention checklist, and pair CSP with secure cookies as covered in Session Management and Secure Cookies.

Go deeper: CSP is one layer of XSS defense. Read Cross-Site Scripting (XSS): Impact and OWASP Prevention for output encoding and the full checklist, and CSRF: Attacks and Defenses for the request-forgery side of browser security.

References

  1. 1OWASP Content Security Policy Cheat Sheet
  2. 2MDN: Content-Security-Policy
  3. 3What Is Cross-Site Scripting (XSS)?: RestingOwl

Q&A Section

A Content Security Policy is a rule set your server sends in an HTTP header that tells the browser which scripts, styles, images, and other resources are allowed to load on a page. Anything not on the allowlist is blocked. Its main purpose is to limit cross-site scripting: even if an attacker injects a script, the browser refuses to run it because it does not match the policy.
No. A strict CSP dramatically reduces the impact of XSS by blocking unauthorized and inline scripts, but it is a mitigation layer, not a complete fix. Weak policies that allow 'unsafe-inline' provide little protection, and some DOM-based sinks can still cause damage. CSP works best combined with output encoding, which stops the injection from being interpreted as code in the first place.
'unsafe-inline' allows any inline script or style to run, which re-opens the exact hole CSP is meant to close, because an injected inline script is also allowed. A nonce is a fresh random value the server puts in both the CSP header and each legitimate inline script tag. Only scripts carrying the matching nonce run, so injected scripts, which cannot know the nonce, are blocked. Nonces are the secure choice.
Deploy it first with the Content-Security-Policy-Report-Only header. In this mode the browser enforces nothing but reports every violation it would have blocked to an endpoint you specify. Watch those reports, fix or allowlist the legitimate resources that would break, and only switch to the enforcing Content-Security-Policy header once the report stream is clean.
Set it as an HTTP response header on your server, reverse proxy, or CDN so it applies to every page. A meta tag equivalent exists but supports fewer directives (for example it cannot set frame-ancestors or report-uri), so the response header is strongly preferred. Generate a fresh nonce per request if you use a nonce-based policy.
Copied!