Cybersecurity & Privacy

Content Security Policy Guide: Build, Test in Report-Only Mode and Roll Out Safely

By Smart Tools Utility Team · Published October 7, 2026 · Updated October 7, 2026 · 9 min read

A Content Security Policy (CSP) is a response header that tells the browser which sources a page may load scripts, styles, images and other resources from, and what it may do with forms and frames. If an attacker manages to inject a <script> tag into your HTML, a good policy makes the browser refuse to run it. A bad or half-finished policy, on the other hand, either protects nothing or breaks your own site. This guide shows how to build a policy with our Content Security Policy (CSP) Generator, how to test it without breaking visitors, and which mistakes to avoid. We checked the behaviour described here by serving test pages with these policies in Chromium and reading the browser's responses.

One note first: a CSP is a second line of defence. It limits the damage of an injection bug; it does not replace escaping output and validating input.

How a policy is written

A policy is a list of directives separated by semicolons. Each directive names a type of resource and lists the sources allowed for it, separated by spaces:

default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:; object-src 'none'

Two kinds of source appear in the list, and they are written differently:

Mixing these up is the classic silent failure. We served a page with script-src self (no quotes): the browser treated self as a host name, so the page's own scripts were not allowed. The generator helps by adding the quotes for the keywords it knows, as described below.

default-src is the fallback for every fetch directive you do not set. If you set script-src, it replaces default-src for scripts; the two are not merged. A few directives, such as frame-ancestors, base-uri and form-action, do not fall back to default-src at all, so you need to set them yourself if you want them restricted.

Building a policy with the generator

  1. Under Directives, type the allowed sources for each directive you need, separated by spaces. A directive left empty is omitted from the policy.
  2. Under Delivery Options, choose HTTP Header or HTML Meta Tag as the output format, and decide whether to turn on Report-Only mode.
  3. Read any warnings that appear above Generated Policy.
  4. Click Copy and add the result to your server or page.

The tool offers 14 directives: default-src, script-src, style-src, img-src, font-src, connect-src, frame-src, frame-ancestors, object-src, base-uri, form-action, media-src, worker-src and report-uri. It starts with a conservative default filled in, which produces:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'
The top of the CSP Generator showing directive rows for default-src, script-src, style-src, img-src, font-src, connect-src, frame-src and frame-ancestors, filled with example sources
The Directives card with a typical policy filled in. Each row takes space-separated sources.

You can type keywords with or without quotes: self and 'self' both come out as 'self'. The tool recognises self, none, unsafe-inline, unsafe-eval, unsafe-hashes, strict-dynamic, report-sample, and anything starting with nonce-, sha256-, sha384- or sha512-. For example, entering self nonce-r4nd0m 'strict-dynamic' sha256-abc123= produced 'self' 'nonce-r4nd0m' 'strict-dynamic' 'sha256-abc123='. Output follows the tool's directive order, not the order you filled them in.

Test first: Report-Only mode

The most important habit when adopting CSP is to not enforce on day one. Turning on the Report-Only mode switch changes the header name to Content-Security-Policy-Report-Only. The browser then evaluates the policy and records violations, but blocks nothing. With a realistic policy, the tool produced:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://fonts.googleapis.com; img-src 'self' data: https:; font-src https://fonts.gstatic.com; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'
The Delivery Options card with HTTP Header selected and the Report-Only switch on, above the Generated Policy box showing a Content-Security-Policy-Report-Only header
With Report-Only on, the generated header name changes and nothing is blocked when it is deployed.

We tested the difference. A test page with one inline script, one inline style and one image from another origin was served three ways. With no policy, everything ran. With the default policy as an enforcing header, the inline scripts were refused, the inline style was not applied (style-src 'self' blocks inline style attributes too), and the cross-origin image was refused. With the same policy as Report-Only, all of it still worked, and the browser logged each problem with a "[Report Only]" prefix in the console and fired a securitypolicyviolation event with a disposition of "report".

A sensible rollout:

  1. Deploy the policy as Report-Only and browse your whole site, including logged-in pages, checkout and embeds. Open the browser console and look for the violation messages.
  2. Fix each violation: allow the legitimate source, or remove the inline code that triggered it. Chromium's message for a blocked inline script includes the SHA-256 hash that would allow exactly that script, which is handy for static inline code.
  3. To collect violations from real visitors, set a reporting endpoint (the tool has a report-uri row), and review the reports for a while, because real traffic includes browser extensions and third-party scripts you will not see yourself.
  4. When the reports are quiet, switch the header name to Content-Security-Policy (turn Report-Only off) and deploy.

Draft your policy and test it in report-only mode

Open the CSP Generator

Why 'unsafe-inline' defeats the purpose

When your site breaks under a strict policy, the quick fix is to add 'unsafe-inline' to script-src. It works, and it also re-allows exactly what an XSS attack needs: an injected inline script. Adding it in the tool triggers this warning: 'unsafe-inline' in script-src significantly weakens XSS protection — consider using a nonce or hash instead if possible. The tool warns for 'unsafe-inline' and 'unsafe-eval' in any directive. We confirmed the effect: with script-src 'self' 'unsafe-inline', our injected inline script ran.

The better options are:

When a nonce or hash is present in a directive, modern browsers ignore 'unsafe-inline' in that same directive, so you can keep it as a fallback for very old browsers. Pair a nonce with 'strict-dynamic' if scripts need to load other scripts; browsers that support it then ignore host allowlists in that directive.

Treat *, https: and data: as warning signs in script-src as well. They allow so many origins that an attacker can usually find a way to host a script. The tool does not warn about these, so check for them yourself. data: in img-src is fine and common.

Header or meta tag?

The HTTP header is the complete way to deliver a policy. A <meta http-equiv="Content-Security-Policy"> tag works when you cannot set headers (some static hosts), with limits. The generator's meta output looks like:

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'">

The CSP specification says frame-ancestors, report-uri and sandbox are ignored in a meta tag, and that Report-Only cannot be delivered that way. Chromium agrees: with those directives in a meta policy, its console says "The Content Security Policy directive 'frame-ancestors' is ignored when delivered via a <meta> element", and the same for report-uri. The generator shows a matching warning in meta mode. Also place the meta tag as early in <head> as possible, before any script or resource it should govern.

Report-Only plus meta. If you switch to HTML Meta Tag while Report-Only is on, the tool shows a warning but still prints a normal enforcing <meta http-equiv="Content-Security-Policy" ...> tag. Do not paste that expecting a dry run: it would enforce the policy. For testing, use the HTTP header.

On Apache or Nginx you add the header in server configuration, for example Header always set Content-Security-Policy "..." on Apache or add_header Content-Security-Policy "..." always; on Nginx. The .htaccess Generator and Security Headers Generator cover the neighbouring headers.

Real-world pitfalls

One cosmetic bug in the tool: the descriptions beside object-src and base-uri contain literal tag names that the page treats as HTML, so those words disappear. We saw Allowed sources for / (commonly 'none') and Restricts URLs usable in a tag. It does not affect the generated policy.

Privacy

The policy is built in your browser from what you type. The tool's code makes no network requests other than the optional feedback form, and the policy text itself is not sent anywhere.

Quick answers

Will a CSP stop all XSS? No. It reduces the impact of many injection bugs, particularly with nonces or hashes, but a permissive policy gives little protection. Keep escaping output.

Can I send two policies? Yes. The browser enforces each enforcing policy, so the result is at least as strict as the strictest one, and Report-Only policies are evaluated alongside them without blocking.

Draft your policy and test it in report-only mode

Open the CSP Generator