Join our Newsletter — 33% off our NHI Course
Home› Guides› Content Security Policy Guide: Writing, Testing and Rolling…
Guide Web & API Security

Content Security Policy Guide: Writing, Testing and Rolling Out CSP

PDF for newsletter subscribers · free to read online

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 26 October 2026 14 min read
On this page

Most Content Security Policies in production either break the site or protect very little, because they were written as long domain allow lists, left in report-only mode forever, or quietly weakened with 'unsafe-inline' to make an error go away. A well-built CSP is one of the few controls that can stop an injected script from running even after an XSS bug ships, and it is the browser-side control behind payment page script management. This guide covers how to write a strict policy, which directives matter, how to collect and triage violation reports, how to roll out without outages, and how to keep the policy strict as the application changes. It is for web developers, front-end leads and AppSec engineers. The Security Headers Guide places CSP among the other response headers; this page goes deep on CSP alone.

Key takeaways

  • Use a strict policy, not an allow list. Nonce- or hash-based script-src with 'strict-dynamic', plus object-src 'none' and base-uri 'none', is the pattern recommended by OWASP and Google because domain allow lists are routinely bypassable.
  • CSP is a second line of defence. It limits what an injected script can do; it does not remove the injection. Output encoding and safe templating come first.
  • Roll out in report-only mode, then enforce. Content-Security-Policy-Report-Only lets you run the strict policy against real traffic and fix breakage before anything is blocked.
  • Reports are a detection feed, not noise. Collect them through Reporting-Endpoints and report-to, filter extension noise, and alert on new script sources on sensitive pages.
  • Add frame-ancestors and Trusted Types once the basics hold. frame-ancestors replaces X-Frame-Options, and require-trusted-types-for 'script' closes DOM XSS sinks that CSP script rules alone cannot.

What CSP does and what it does not do

Content Security Policy is a response header (or, with limits, a <meta> element) that tells the browser which sources of script, style, frames, connections and other resources the page may use, and which navigation and embedding are allowed. The current specification is CSP Level 3, a W3C Working Draft. When an injected script does not carry a valid nonce or hash, a strict policy stops it executing, which turns many reflected, stored and DOM XSS bugs from exploitable into blocked and reported.

CSP does not fix the underlying bug, does not stop HTML injection that steals data without script (dangling markup and form hijacking are only partly covered by form-action and other directives), does not protect against a compromised script you deliberately trust, and does not apply to non-browser API clients. The OWASP cheat sheet is explicit that CSP is not a replacement for secure coding. See the Cross-Site Scripting (XSS) Prevention Guide and the Input Validation and Output Encoding Guide.

ThreatWhat CSP contributesDirectiveStill needed
Reflected and stored XSS (MITRE ATT&CK T1059.007 JavaScript)Blocks injected inline and external scripts without a nonce or hashscript-src with nonce or hash and 'strict-dynamic'Contextual output encoding
DOM XSS through innerHTML and similar sinksTrusted Types force sink assignments through reviewed policiesrequire-trusted-types-for, trusted-typesSafe DOM APIs, sanitiser
Base tag injection redirecting relative script URLsPrevents an injected <base> changing where scripts load frombase-uri 'none' or 'self'Output encoding
Plugin contentBlocks <object> and <embed>object-src 'none'None in modern browsers
ClickjackingControls which origins may frame the pageframe-ancestorsHeader delivery (not meta)
Form hijacking to attacker hostsLimits where forms may submitform-actionEncoding
Data exfiltration and skimming by injected or tampered script (T1185 Browser Session Hijacking)Limits where script can send dataconnect-src, img-src, default-srcScript inventory and integrity monitoring
Mixed content during HTTPS migrationUpgrades HTTP subresource requestsupgrade-insecure-requestsHSTS

Strict CSP versus allow-list CSP

An allow-list policy such as script-src 'self' cdn.example.com www.googleapis.com trusts every script on every listed host. Large shared hosts serve JSONP endpoints, old library versions and user content that an attacker can use to run code within the policy, so allow lists are hard to keep both working and safe. Google's web.dev guidance and the OWASP cheat sheet both recommend strict policies instead.

ApproachPolicy coreBest forTrade-offs
Nonce-based strictscript-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none'Server-rendered pages where each response can carry a fresh nonceNeeds templating changes; responses with nonces cannot be cached and shared between users as-is
Hash-based strictscript-src 'sha256-{hash}' 'strict-dynamic'; object-src 'none'; base-uri 'none'Static sites, CDN-cached pages and single-page app shellsAny change to an inline script, even whitespace, changes the hash; needs a build step
Allow listscript-src 'self' host1 host2Legacy sites as an interim step, or as a backstop for directives other than scriptBypassable through hosted gadgets; grows without bound
Allow list with 'unsafe-inline'script-src 'self' 'unsafe-inline'NothingOffers almost no XSS protection; remove it

'strict-dynamic' lets a script that is already trusted (by nonce or hash) load further scripts it creates, so tag managers and loaders work without listing every host. In browsers that support it, host allow lists and 'unsafe-inline' in the same directive are ignored. That is why web.dev suggests script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline' when you must support old browsers: modern ones ignore the fallbacks, older ones fall back to a weaker policy rather than breaking.

Writing the policy: directives that matter

A practical starting policy

For a server-rendered application, a reasonable first strict policy delivered as a header is:

Content-Security-Policy: script-src 'nonce-R4nd0m' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'; form-action 'self'; report-to csp-endpoint

with Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports". Add default-src, connect-src, img-src and style-src restrictions later, once script control is enforced, because each one adds breakage risk for less XSS protection.

Nonce rules

  • Fresh per response: generate a new nonce for every HTTP response with a cryptographically secure generator (ASVS 5.0 V11.5.1 asks for at least 128 bits of entropy for non-guessable values). A nonce reused across responses or cached in shared HTML is a key an attacker can read and reuse.
  • Applied by the template, not by a filter: the OWASP cheat sheet warns against middleware that adds the nonce to every script tag in the output, because injected scripts would then receive it too.
  • Exact match: the browser compares the nonce as a string; it does not decode it.

Hash rules

  • Generate in the build: compute sha256, sha384 or sha512 hashes of each inline script during the build and inject them into the header or meta tag.
  • External scripts: under CSP Level 3 a hash can also allow an external script whose <script> element carries matching Subresource Integrity metadata.

Refactoring code that strict CSP blocks

  • Inline event handlers: onclick="..." attributes are blocked; move them to addEventListener calls in nonce'd or external scripts. 'unsafe-hashes' exists for handlers you cannot move, but treat it as a temporary exception.
  • javascript: URLs: replace with event listeners.
  • eval() and new Function(): replace with JSON.parse() or precompiled templates. Avoid adding 'unsafe-eval'; if WebAssembly needs compilation, use the narrower 'wasm-unsafe-eval'.
  • Inline styles: style-src restrictions break many component libraries. Treat style control as a later phase; script control delivers most of the value.

Header versus meta delivery

A policy in a <meta http-equiv> element supports most script controls, but per CSP Level 3 it cannot carry frame-ancestors, sandbox or report-uri, cannot be report-only, and does not apply to content before it in the document. Use the header for anything beyond a static site, and place a meta policy as early as possible in <head> if you must use one.

Reporting and violation triage

Reporting is what turns CSP from a configuration into a monitoring control. ASVS 5.0 V3.4.7 requires the policy to specify a location to report violations.

  • Endpoints: declare an HTTPS endpoint with Reporting-Endpoints: csp-endpoint="https://..." and reference it with report-to csp-endpoint. report-uri is deprecated in CSP Level 3 but older browsers only understand it, so the OWASP cheat sheet suggests sending both during transition. Reporting-Endpoints replaces the older Report-To header.
  • Samples: add 'report-sample' to script-src to receive the first 40 characters of a blocked inline script, which is usually enough to tell a browser extension from a real injection.
  • Fields to keep: document URL, blocked URL, effective directive, disposition (enforce or report), source file and line where present, and user agent. Store them like any other security log.
  • Filter noise: browser extensions, injected antivirus scripts and translation tools generate large volumes of reports with chrome-extension: or similar sources. Filter by scheme and known patterns, but do not discard reports from your own sensitive pages without review.
  • Protect the endpoint: it accepts unauthenticated POSTs from the internet, so rate-limit it, cap body size and treat report content as untrusted input in any dashboard. See the Rate Limiting and API Abuse Guide.
Violation patternLikely causeAction
Inline script blocked on many pages, same sampleLegitimate inline script without nonce or hashAdd nonce in the template or move to an external file
Blocked chrome-extension: or moz-extension: sourceUser's browser extensionFilter out of alerting
Blocked evalLibrary using eval or new FunctionUpgrade or replace the library, avoid 'unsafe-eval'
New external host on checkout or login pageNew tag, compromised third party or injected skimmerInvestigate the same day
Inline script blocked with an unfamiliar sample on one URLPossible XSS attemptCorrelate with WAF and application logs, test the parameter

Rolling out CSP without breaking production

  1. Inventory scripts (week 1): list every first-party and third-party script per page template, who owns it and why it is there. Payment, login and account pages first.
  2. Refactor (weeks 2 to 6): add nonces in templates or hashes in the build, remove inline handlers and javascript: URLs, replace eval.
  3. Report-only on real traffic (2 to 4 weeks): send the strict policy as Content-Security-Policy-Report-Only. You can send it alongside an existing enforced policy, so a weak current policy stays in place while you test the strict one.
  4. Validate: check the policy with Google's CSP Evaluator or Lighthouse, and add an automated test that fails the build if the header is missing or contains 'unsafe-inline' without a nonce or hash.
  5. Enforce in rings: switch to Content-Security-Policy on one low-risk template or a percentage of traffic, watch report volume and error budgets for a week, then widen.
  6. Roll back cleanly: keep the header in configuration (gateway, CDN or framework setting) so you can switch a template back to report-only in minutes, rather than editing code under pressure. Rolling back to report-only keeps visibility; removing the header loses it.
  7. Tighten in phases: after script control is stable, add frame-ancestors, form-action, then connect-src and img-src on sensitive pages, then Trusted Types.

Small sites with few third-party scripts can often go from report-only to enforced in one release with a hash-based policy generated at build time. Large estates with many teams and tag managers need a central policy service or shared middleware, a per-application owner, and policy changes reviewed like code. Single-page applications usually suit a hash-based policy on the shell plus 'strict-dynamic'; check framework guidance, for example the React and Next.js Security Guide, Angular Security Guide and Vue Security Guide.

Beyond scripts: frame-ancestors, Trusted Types and third-party code

frame-ancestors

ASVS 5.0 V3.4.6 requires frame-ancestors on every response so the application cannot be embedded by default. Use 'none' for pages that should never be framed and 'self' or named origins where embedding is a feature. It must be delivered in a header. Keep X-Frame-Options only for very old browsers. See the Clickjacking Guide.

Trusted Types

DOM XSS happens in client code that assigns strings to sinks such as innerHTML, eval or script.src. With require-trusted-types-for 'script', those assignments throw a TypeError unless the value comes from a Trusted Types policy; trusted-types lists the policy names the code may create. A policy named default can catch legacy string assignments during migration. MDN lists Trusted Types as Baseline newly available since February 2026, so older browsers will not enforce it; test with enforcement on a supporting browser and roll out in report-only mode first like the rest of CSP.

Third-party scripts and payment pages

A strict CSP with 'strict-dynamic' trusts whatever your trusted scripts load, so a compromised tag manager or analytics script is still a supply chain path into the page. Pair CSP with a script inventory, Subresource Integrity on fixed-version scripts, restrictive connect-src on payment pages, and change monitoring. PCI DSS v4.0.1 requirement 6.4.3 requires payment page scripts to be authorised, integrity-checked and inventoried, and 11.6.1 requires a mechanism to detect unauthorised changes to security-impacting HTTP headers and payment page content; the PCI SSC published an information supplement, "Payment Page Security and Preventing E-Skimming", on these two requirements in 2025. CSP and its reports are one common way to meet part of them, not the whole answer. See the Software Supply Chain Security Guide.

Detection and measurement

  • Monitor: violation volume per template and directive, new blocked hosts on sensitive pages, enforced versus report-only coverage, and responses served without the header.
  • Correlate: pair violations with WAF events and application logs for the same URL and time. See the Web Application Firewall Guide.

Metrics that show CSP is working:

  • Percentage of HTML responses with an enforced strict CSP (nonce or hash, object-src 'none', base-uri set).
  • Number of applications with 'unsafe-inline' or 'unsafe-eval' effective in script-src (target zero).
  • Time from a new third-party script appearing on a payment or login page to review.
  • Percentage of XSS findings from testing that the enforced policy would have blocked.
  • Time spent in report-only before enforcement, per application (a policy stuck in report-only protects nothing).

Common failure patterns

  • 'unsafe-inline' left in to fix one error: without a nonce or hash in the same directive, it disables most XSS protection.
  • Permanent report-only: the policy was tested, never enforced, and nobody reads the reports.
  • Static or cached nonces: the same nonce in every response, or HTML with a nonce cached at the CDN and served to everyone.
  • Auto-nonce middleware: a filter that stamps the nonce on every script tag, including injected ones.
  • Allow lists of large shared hosts: broad CDN or API domains that host content an attacker can reuse to run script within policy.
  • Missing base-uri and object-src: a strict script-src undermined by base tag injection or plugin content.
  • Multiple conflicting headers: a gateway and the application each set a policy; browsers enforce both, so pages break unpredictably and teams respond by loosening one.

Framework mapping

FrameworkReferenceHow CSP supports it
OWASP ASVS 5.0V3.1.1, V3.4.3, V3.4.6, V3.4.7Documented browser security features, CSP present, frame-ancestors on every response, violation reporting
NIST CSF 2.0PR.PS-06Secure software development practices
NIST SP 800-53 Rev. 5SA-8, SI-10, SI-7Security engineering principles, input handling, integrity of software and information
CIS Controls v8.1Control 16 Application Software SecuritySecure application development and hardening
ISO/IEC 27001:2022 Annex AA.8.26, A.8.28Application security requirements, secure coding
PCI DSS v4.0.16.4.3, 11.6.1Payment page script management and change detection
MITRE ATT&CKT1059.007, T1185, T1189Limits injected JavaScript execution, browser session hijacking and drive-by script delivery

Practitioner checklist

  • Every HTML response carries a CSP header with a nonce- or hash-based script-src and 'strict-dynamic'.
  • object-src 'none' and base-uri 'none' (or 'self') are present everywhere.
  • Nonces are generated per response from a secure random source and applied by the template engine.
  • No effective 'unsafe-inline' or 'unsafe-eval' in script-src; exceptions are time-limited and owned.
  • frame-ancestors is set in the header on every response.
  • Violation reports flow through Reporting-Endpoints to a rate-limited endpoint and into the security log platform.
  • New script sources on payment and login pages raise an alert reviewed within one working day.
  • CI fails if the header is missing or weakened; the policy is reviewed in CSP Evaluator after every change.
  • Trusted Types are piloted in report-only mode on the main single-page application.

Standards and references

Related NHI Mgmt Group resources: Security Headers Guide · Cross-Site Scripting (XSS) Prevention Guide · Clickjacking Guide · Secure API Design Guide

PDF for subscribers

Take this guide with you

Every section, table and checklist in one PDF, with links to the sources and related guides. Subscribe to our weekly NHI & AI Identity Journal and we’ll email you a download link.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 26 October 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org

PDF Downloads for Newsletter Subscribers

Get the guide

Enter your details and we’ll email you a download link.

The full guide is free to read online. The PDF is a thank you for subscribing.

Fill in all fields marked * and tick the PDF box to continue.