← Back to blog

Content Security Policy Guide: Build, Test, Enforce

October 21, 2026
Content Security Policy Guide: Build, Test, Enforce

A Content Security Policy (CSP) is an HTTP response header that tells the browser which sources it may load scripts, styles, images and other resources from. If an attacker injects markup into your page, the browser refuses to run what the policy does not allow. The safe way to adopt it: ship it in report-only mode, read the violation reports, fix or allow what is legitimate, then enforce. This guide walks through that path, from the first header to a strict nonce-based policy.

Pingfloat
Check Your Site’s Security Signals
Pingfloat provides SSL certificate checks and website uptime monitoring to help developers and IT professionals assess website health.
Explore Pingfloat tools

TL;DR

  • CSP is a defense-in-depth layer against cross-site scripting (XSS) and data injection. It does not replace input validation and output encoding.
  • Start with Content-Security-Policy-Report-Only and a reporting endpoint, so nothing breaks while you learn what your pages load.
  • For anything high-risk, move from domain allowlists to a strict policy: a per-response nonce (or hashes), 'strict-dynamic', object-src 'none' and base-uri 'none'.
  • Never cache a page with a nonce baked in. A reused nonce gives you no protection.
  • Verify what your server actually sends with a response header inspector, and keep monitoring after you enforce.

Table of Contents

What a Content Security Policy does

Per MDN's CSP guide, CSP is a set of instructions from a site to the browser that limits which origins resources can load from. It is designed to prevent or minimize certain classes of attack, with cross-site scripting and data injection the best known. The W3C Content Security Policy Level 3 specification describes it as a tool for locking down an application and mitigating content injection vulnerabilities.

The mechanism is simple. Your server sends a header such as:

Content-Security-Policy: default-src 'self'; img-src 'self' https://images.example.com

The browser reads it and blocks anything outside those rules: an inline script an attacker slipped into a comment field, a script from an unknown host, a form that posts to a third-party domain. The blocked attempt shows up as a console error and, if you configured it, as a report sent to your server.

The directives you will use most

CSP is built from fetch directives (which control where resource types may load from) plus a few that control other behavior. The ones that matter in most real policies:

DirectiveWhat it controlsTypical starting value
default-srcFallback for any fetch directive you do not set'self'
script-srcJavaScript sources, including inline scripts'self', or a nonce plus 'strict-dynamic'
style-srcStylesheets and inline styles'self'
img-srcImages'self' data: plus your image CDN
connect-srcFetch, XHR, WebSocket targets'self' plus your API hosts
object-srcPlugins such as <object> and <embed>'none'
base-uriAllowed values for the <base> element'none' or 'self'
frame-ancestorsWho may embed your page in a frame'self' or 'none'
report-toNamed endpoint that receives violation reportsAn endpoint you define

If you set default-src and nothing else, every resource type inherits it. That is a fine first draft, and also the reason a first draft usually breaks something: analytics, fonts, a chat widget and an inline onclick handler all fall outside 'self'.

What CSP will not do

CSP is a safety net, not the primary fix. CISA and the FBI's Secure by Design alert on eliminating cross-site scripting (September 2024) points to the root cause of XSS: failures in input validation, sanitization and output encoding. Fix those first. Treat CSP as the layer that catches what slips through.

That framing changes how you scope the work. You are not trying to build a perfect policy on day one. You are trying to shrink the damage an injection can do, and to get there without taking your own site down.

Step 1: Deploy in report-only mode

The Content-Security-Policy-Report-Only header evaluates a policy and reports violations without blocking anything. MDN's reference for the header says it exists so you can test or repair violations before a policy is enforced. This is the answer to the common question about CSP report-only mode: it is the same policy syntax, a different header name, and the browser logs instead of blocks.

A minimal setup has two headers:

Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint

Two details trip people up:

  1. The report-to directive is required for reports to be sent. Per MDN, without it the report-only header has no effect beyond the browser console. Reports go to endpoints named in the Reporting-Endpoints header.
  2. report-uri is deprecated. It still works in some browsers and its report format differs slightly, but report-to is the modern route. Some teams send both during a transition.

Violation reports arrive as JSON through HTTP POST requests, so your endpoint needs to accept and store them. You can build one yourself or use a hosted collector. Either way, store the blocked URL, the violated directive and the page it happened on.

How long to stay in report-only

The MDN implementation guide recommends starting in report-only mode. In practice you watch reports for days or weeks, long enough to capture rarely visited pages, logged-in areas, checkout flows and seasonal campaigns. Move to enforcement once new violations from legitimate resources stop arriving. Skipping this phase is the main reason teams give up on CSP after it breaks a feature.

Expect noise. Browser extensions inject scripts and styles into pages, and those generate reports that are not your problem. Filter by blocked URL schemes such as chrome-extension: before you conclude your site has 4,000 violations.

Step 2: Triage what the reports tell you

Go through the violations in groups rather than one at a time.

  1. Resources you own and expected. Your CDN, your API, your image host. Add the origin to the right directive.
  2. Third-party tools you chose. Analytics, payments, support chat, video embeds. Add the specific origin, not a wildcard, and ask whether you still need the tool.
  3. Inline code. Inline <script> blocks, inline event handlers and javascript: URLs are blocked by default under a meaningful script-src. Move them to files, or give inline scripts a nonce or hash.
  4. Things you do not recognize. Investigate these first. An unknown script host in your reports is either a forgotten tag-manager entry or the exact kind of injection CSP exists to catch.

Mixed content often surfaces here too. If your reports show http:// resources on HTTPS pages, the cleanest fix is removing them at the source. This write-up on fixing mixed content with CSP first, then removing HTTP at its source shows how to use the policy for discovery before cleanup, and our own piece on fixing mixed content with DevTools, crawlers and monitoring covers the manual side.

Step 3: Move from allowlists to a strict policy

A host allowlist looks reassuring and ages badly. A single allowed CDN or domain that hosts user-uploaded or reusable scripts can hand an attacker a way around the policy. OWASP's frontend supply chain handbook recommends moving toward strict policies based on cryptographic trust rather than brittle allowlists, so that only code you authorized can run even if an attacker injects a tag.

web.dev's strict CSP article defines a strict CSP as one based on nonces or hashes. The nonce-based structure looks like this:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

The OWASP Web Security Testing Guide covers how to test for CSP in a deployed application, and uses the same structure as a moderate strict policy.

How the pieces work

  • Nonce. A random, unguessable, base64-encoded value generated on the server for every HTTP response. You put it in the header and on each legitimate script tag: <script nonce="r4nd0m" src="/app.js"></script>. A script without the matching nonce does not run.
  • Hashes. For a static inline script, you can instead list its SHA-256 hash in script-src. This suits pages that are fully static, where a per-response nonce is awkward.
  • 'strict-dynamic'. Trust propagates from a nonced or hashed script to the scripts it loads dynamically. This removes the need for long domain lists.
  • object-src 'none' and base-uri 'none'. These close two classic bypasses: legacy plugin content and injected <base> tags that redirect relative script URLs.

Two mistakes that quietly undo a strict policy

Nonce reuse. The nonce must be unique per response. If you cache a rendered page with its nonce, or generate one per session or per minute, an attacker can replay or predict it and the protection is gone. Check your CDN and full-page cache rules before you ship. Pages with nonces should be excluded from shared caching, or the nonce should be injected at the edge on every request.

Treating 'strict-dynamic' as magic. It creates a chain of trust. If a nonced script loads a library that an attacker controls, that library inherits full execution rights. Load only third-party code you trust and pin it, ideally with Subresource Integrity, from versioned, immutable URLs rather than a floating latest.

Step 4: Enforce, then keep watching

When the report volume from legitimate traffic is quiet, rename the header from Content-Security-Policy-Report-Only to Content-Security-Policy. A cautious pattern is to run both at once: enforce the policy you trust, and keep a stricter candidate in report-only mode to see what tightening would break. The two headers are independent.

After enforcement:

  • Keep the report-to directive on the enforced policy so you still see blocks in production.
  • Re-check after every change to third-party tags, framework upgrades or CDN moves. New marketing scripts are the most common source of fresh violations.
  • Confirm the header reaches users unchanged. Proxies, CDNs and frameworks sometimes drop, duplicate or rewrite headers, so test the live response, not just your config file.

Perspective: why we treat CSP as an operations problem

Most CSP guides focus on syntax. In our experience the policy string is the easy part. The hard parts are operational: knowing what your pages really load, noticing when a header silently disappears after a deploy, and keeping the policy true as the site changes.

Perspective: why we treat CSP as an operations problem — overview diagram

That is why we build tools around the live response rather than the config. Pingfloat's Response Headers Inspector lets you request a URL and read the cache and security headers it returns, which is the fastest way to see whether your CSP, and the header next to it, is what you think it is. Our broader web security checklist puts CSP in the context of the other controls a site needs.

Our advice is to resist the urge to perfect the policy before shipping it. Get a report-only header live this week, read the reports, and let real data tell you what to allow. A modest enforced policy beats an ambitious one that never leaves the staging branch.

— Andreas

Check your headers and keep the site up

A policy that blocks a critical script can look like an outage, and a misconfigured header can vanish after a deploy. Pingfloat's free Response Headers Inspector shows what your server sends, and the Website Uptime Monitor helps you notice quickly when a release breaks a page. The core tools run without an account or installation. You can browse the full set on the Pingfloat home page.

A worked rollout example

Suppose you run a marketing site with a few pages of static content, an analytics script, an embedded video and a contact form that posts to your own API. Here is how the rollout might go.

Four-week content security policy rollout

Week 1. Ship this in report-only mode:

Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint

Reports show violations for the analytics host, the video host, your font provider and several inline scripts.

Week 2. Rewrite the policy with explicit directives: script-src gets a nonce and 'strict-dynamic', img-src gets 'self' data:, frame-src gets the video host only, connect-src gets your API. Inline scripts get nonces from your template layer. Keep report-only on.

Week 3. Reports drop to extension noise and one forgotten legacy widget. Remove the widget or give it a proper source, and verify with the inspector that the header is present on every page type, including error pages.

Week 4. Enforce. Keep report-to active, and add a calendar reminder to review reports monthly.

The point is the rhythm: change the policy, watch the reports, adjust, enforce. It is slow only if you start with enforcement.

Common pitfalls

  • Adding 'unsafe-inline' to make errors disappear. It allows inline scripts, which is exactly what XSS payloads are. If you must keep it temporarily, know that modern browsers ignore it when a nonce or hash is present, so it can serve as a transitional fallback, but do not count it as protection.
  • Wildcard hosts. An entry like https://*.example.com trusts every subdomain, including ones you have forgotten about.
  • Setting CSP on HTML only and forgetting error pages. A 404 or 500 page that lacks the header is still part of your attack surface. If those errors are a concern, see our references on 500 Internal Server Error and 503 Service Unavailable.
  • Forgetting that CSP applies per response. An API endpoint, a static file and an HTML page may each need different headers.
  • Relying on CSP in place of fixing the bug. The injection still exists. CSP limits what it can do.

FAQ

What is the difference between Content-Security-Policy and Content-Security-Policy-Report-Only?

The first enforces the policy and blocks violating resources. The second applies the same rules but only reports violations, so nothing breaks. Use report-only to learn what your site loads, then switch the header name when you are ready to enforce.

Why am I not receiving any CSP reports?

The most common cause is a missing report-to directive. According to MDN, it must be present for report-only reports to be sent. Also check that the Reporting-Endpoints header defines the endpoint name you reference, and that your endpoint accepts POST requests with JSON bodies.

How do I view a site's Content-Security-Policy?

Open your browser's developer tools, go to the Network tab, select the document request and read its response headers. You can also use a response header inspector to request a URL and list its headers without opening DevTools. Remember that a policy can also be set through a meta element, which will not appear in the headers.

Is a strict CSP better than an allowlist?

For high-risk applications, yes. OWASP and web.dev both favor nonce or hash-based policies with 'strict-dynamic' over long domain allowlists, because allowlists often include domains that can be abused. A strict policy is more work to implement, so use report-only mode to get there safely.

Does CSP stop all XSS?

No. CISA and the FBI say it should not be a substitute for secure coding. Validate input and encode output in your application, and use CSP as the second layer.

Sources