Content-Security-Policy Generator

Directives in, a deployable CSP header out - with the usual self-defeating mistakes flagged.

Written against
CSP Level 3 (W3C Working Draft) and CSP Level 2 (W3C Recommendation, 2016)
Behaviour verified in
Chrome, Firefox, Safari and Edge
Last reviewed
August 2026
Where it runs
Your browser tab. No policy is uploaded.
Start from a policy that is known to work
Directives
Options

The header — csp.txt

What this generator is for

Build a Content-Security-Policy directive by directive and output it as an HTTP header, an Apache or nginx line, a PHP call or a meta tag. The notes call out the settings that quietly defeat the policy - unsafe-inline, a bare wildcard, a missing default-src - and the directives a meta tag cannot express at all. Start in report-only mode; the tool explains why.

Deploy it in report-only mode first

Content-Security-Policy-Report-Only sends the browser the same policy and asks it to report violations instead of enforcing them. Nothing breaks, and your report endpoint fills up with every inline script, third-party widget and analytics beacon you had forgotten about. Read that list for a week, fix the policy, then switch to the enforcing header.

You can ship both headers at once - report-only for the policy you are working towards, enforcing for the one you already trust. That is how you tighten a policy on a live site without a maintenance window.

unsafe-inline is the whole ballgame

A CSP exists to stop injected script from executing. 'unsafe-inline' in script-src allows every inline <script> block and every onclick attribute - which is precisely the mechanism XSS uses. A policy with it is documentation, not a defense.

The way out is a nonce: put a fresh random value in the header on every response (script-src 'nonce-abc123') and the same value on each script tag you trust. It has to be per-response and unpredictable, or an attacker just reads the nonce out of the page and reuses it. Styles are the harder half in practice, since a lot of libraries write inline style attributes.

Four directives that cost nothing and close the most doors

object-src 'none' and base-uri 'none' are almost free and both stop real attacks. Legacy plugin content is the reason object-src exists and nothing on a modern site needs it. base-uri matters for a subtler reason: an injected <base> tag rewrites every relative URL on the page, so an attacker who can inject one tag can repoint all of your relative script sources at their own host without ever touching script-src.

The other two are about where the page can go rather than what it can load. frame-ancestors 'none' is clickjacking protection and replaces X-Frame-Options; form-action 'self' stops an injected form from posting your visitor's password somewhere else. Neither inherits from default-src, so a policy without them is unrestricted on both counts however strict the rest of it looks. That is the most common gap in policies that otherwise look complete.

What a CSP does not do

It is a second line of defence, not the first. A CSP sanitises nothing: the fix for cross-site scripting is still contextual output encoding, and the policy is what limits the damage on the day that fails. Selling it internally as "we have a CSP, so XSS is handled" is how a real hole survives a review.

It also does not stop everything a script can do once it runs. connect-src restricts fetch and WebSocket, but a determined payload can still move data out through a navigation or a resource load the policy does allow, and the directive meant to cover that case - navigate-to - was never shipped by browsers. Nothing in a CSP touches browser extensions either; they inject script on the visitor's side and will fill your report endpoint proving it.

How a policy rots

Policies rarely fail on the day they ship. They fail three months later, when someone adds a tag manager and the fastest way to make it work is to append a host - or, when that does not work, 'unsafe-inline'. The end state is a header long enough that nobody reads it and permissive enough that it blocks nothing.

Two habits prevent most of that. Keep the policy in version control next to the code rather than in a hosting control panel, so every change is a reviewable diff with a reason attached. And keep the report endpoint alive after rollout: a spike in violations for a directive nobody touched is the earliest signal you get that a third-party script has started doing something new.

Every directive this tool writes, and what happens when you leave it out

The fallback column is the part that catches people. Some directives inherit from default-src when they are absent; the ones that do not are unrestricted until you name them, which is how a policy that looks complete leaves a hole.

Directive Controls If omitted Meta tag
default-src The fallback for the fetch directives below it. Nothing is restricted unless a specific directive names it. Yes
script-src Scripts, including inline blocks and event handlers. Falls back to default-src. Yes
style-src Stylesheets, <style> blocks and style attributes. Falls back to default-src. Yes
img-src Images, favicons and any data: image URIs. Falls back to default-src. Yes
font-src Font files requested by @font-face. Falls back to default-src. Yes
connect-src fetch, XHR, WebSocket, EventSource and sendBeacon. Falls back to default-src. Yes
media-src <audio>, <video> and <track> sources. Falls back to default-src. Yes
object-src <object>, <embed> and legacy plugin content. Falls back to default-src. Set it to 'none' anyway. Yes
frame-src What this page may load in an iframe. Falls back to child-src, then default-src. Yes
worker-src Web workers, shared workers and service workers. Falls back to child-src, then script-src, then default-src. Yes
manifest-src The web app manifest. Falls back to default-src. Yes
frame-ancestors Who may put this page in an iframe. Replaces X-Frame-Options. Unrestricted. default-src does NOT cover it. Ignored
form-action Where a <form> is allowed to submit. Unrestricted. default-src does NOT cover it. Yes
base-uri What a <base> tag may set the document base to. Unrestricted. default-src does NOT cover it. Yes
report-uri Where violation reports are POSTed. Deprecated but widely supported. Violations are logged to the console only. Ignored
report-to The named reporting group, paired with a Reporting-Endpoints header. Violations are logged to the console only. Ignored

Three policies for stacks that actually exist

A server-rendered site with no third parties

Everything is served from one origin and there is no inline script, because the templates put their JavaScript in files. This is the policy every project should be able to reach, and the one most projects are two refactors away from.

Content-Security-Policy:
  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 'none';
  upgrade-insecure-requests

data: is in img-src for inline SVG icons and favicons; drop it if you have none. Note that 'self' does not include subdomains -- static.example.com needs naming.

Analytics, a tag manager, Google Fonts and a CDN

The common marketing stack, and the reason most real policies contain host allowlists. Each vendor needs a directive for the script and often a second one for what the script then talks to, which is the part people miss: adding the script host to script-src and forgetting connect-src produces a tag that loads and then fails silently.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example.com https://www.googletagmanager.com;
  style-src 'self' https://cdn.example.com https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https://www.googletagmanager.com https://*.google-analytics.com;
  connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'self'

Google Fonts is two hosts on purpose: the stylesheet comes from fonts.googleapis.com and the font files from fonts.gstatic.com, so a policy with only the first one loads the CSS and then blocks every @font-face in it.

A checkout page with Stripe, on a nonce

A payment page is where a policy earns its keep, and it is also where inline script is hardest to avoid. The answer is a per-response nonce rather than 'unsafe-inline', plus 'strict-dynamic' so the vendor script may load the further scripts it needs without you allowlisting hosts you cannot enumerate.

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-r4nd0mPerResponse' 'strict-dynamic' https: 'unsafe-inline';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https://*.stripe.com;
  connect-src 'self' https://api.stripe.com https://maps.googleapis.com;
  frame-src https://js.stripe.com https://hooks.stripe.com;
  object-src 'none';
  base-uri 'none';
  form-action 'self';
  frame-ancestors 'none'

The https: and 'unsafe-inline' tokens in script-src look like mistakes and are not: a browser that understands nonces ignores 'unsafe-inline', and one that understands 'strict-dynamic' ignores the host allowlist. They are there so an old browser degrades to a weaker policy instead of a broken page. This is the pattern Google publishes as a strict CSP.

Keeping inline script without giving the policy away

A nonce has to be generated per response

The nonce is not a secret and it is not a password -- it is a value an attacker cannot guess in advance. Generate it once per request, put it in the header and on every script tag you trust, and never cache the HTML and the header separately, because a cached page with a stale nonce is a blank page.

Sixteen random bytes is the usual recommendation. Anything derived from the request -- a timestamp, a session id, a hash of the URL -- is predictable and therefore not a nonce.

<?php
// One nonce per response, before any output.
$nonce = base64_encode(random_bytes(16));

header(
    "Content-Security-Policy: "
    . "default-src 'self'; "
    . "script-src 'nonce-{$nonce}' 'strict-dynamic' https: 'unsafe-inline'; "
    . "object-src 'none'; base-uri 'none'"
);
?>
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES) ?>">
  // This block runs. One without the attribute does not.
</script>

On a proxy, generate it where the HTML is generated

nginx can mint a nonce with ngx_http_sub_module or a Lua block, but the value has to reach both the header and the markup, and only the application knows the markup. The workable split is: application sets the header and the attributes, server config handles the static-file case where there is no inline script at all.

If you must set the policy in nginx, set the static one there and let the application override it for HTML responses. Two CSP headers do not merge into the looser of the two -- the browser enforces both, and only what satisfies every policy is allowed, which is usually not what the second header intended.

# nginx: policy for static assets only.
location ~* \.(?:css|js|woff2|png|svg)$ {
    add_header Content-Security-Policy "default-src 'none'" always;
}

# HTML is left to the application, which owns the nonce.
location / {
    proxy_pass http://app;
}

Hashes, when the inline block never changes

A build-time inline script -- a theme switcher, a bit of critical CSS -- can be allowed by the SHA-256 of its exact contents instead of a nonce. The hash covers the text between the tags only: no attributes, no surrounding whitespace differences. Change one character and the block stops running, which is either the point or an outage, depending on whether the hash is generated by your build.

Inline event handlers (onclick and friends) are a separate case: hashing them needs 'unsafe-hashes', which loosens the policy meaningfully. Moving them to addEventListener is nearly always the better trade.

# The hash of the exact inline body, base64-encoded:
printf %s 'document.documentElement.dataset.theme="dark"' \
  | openssl dgst -sha256 -binary | openssl base64

# Then, in the policy:
script-src 'self' 'sha256-Xp7A1s2Kk9Qm0bZ8vN3rL6cJdF3wA1uEoIgTzKbSxPk='

Collect the reports, then read them

report-uri is deprecated in favour of the Reporting API, and browser support is split in a way that makes shipping both the sensible answer today: report-to plus a Reporting-Endpoints header for Chromium, report-uri for everything else.

Budget for noise. A large share of real-world reports come from browser extensions injecting script into your page, and they are indistinguishable from an attack in the report body except by the blocked URI scheme. Filter chrome-extension:, moz-extension: and safari-extension: before anyone looks at the numbers, or the first week of data will bury the two reports that mattered.

Reporting-Endpoints: csp="/csp-report"
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp; report-uri /csp-report

# What lands on /csp-report (classic format):
{
  "csp-report": {
    "document-uri":       "https://example.com/checkout",
    "effective-directive": "script-src-elem",
    "blocked-uri":        "https://widget.vendor.com/embed.js",
    "disposition":        "report",
    "original-policy":    "default-src 'self'; report-uri /csp-report"
  }
}

What breaks first, in the order it usually breaks

  1. Inline <script> blocks and onclick attributes

    Move them to a file, or give the blocks a nonce. Event-handler attributes cannot take a nonce at all, which is why they are the last thing to be fixed and the reason most people reach for 'unsafe-inline'.

  2. Inline style attributes written by JavaScript libraries

    A nonce does not help here: it applies to <style> elements, not to style attributes. Either allow 'unsafe-inline' in style-src as a deliberate, documented compromise, or replace the library. Style injection is a far smaller risk than script injection, which is why this is the compromise most policies make.

  3. Third-party embeds: chat widgets, maps, video, tag managers

    Each one needs its script host in script-src, its API host in connect-src, and an iframe host in frame-src if it renders one. Read the vendor documentation; several publish their own CSP requirements.

  4. Fonts loaded from a different origin than the stylesheet

    font-src is a separate directive and does not inherit from style-src. The Google Fonts pair is the classic case.

  5. Libraries that call eval or new Function

    Some template engines and older date pickers do. 'unsafe-eval' makes them work and removes a real protection; check for a build-time-compiled mode first, which most of them now have.

  6. Source maps and hot reload in development

    A dev server needs a WebSocket in connect-src. Keep a separate, looser policy for development rather than loosening production to match, or the compromise ships.

Questions that come up on every rollout

Where should the header be set - server or application?

Either works. The server config (Apache, nginx) is simpler and covers static files too. The application is the right place once you need a per-response nonce, because the value has to change on every request. Do not set it in both: two policies do not merge - the browser enforces both, and only what passes each one is allowed.

Does a meta tag work as well as the header?

Mostly, and it is a reasonable option on shared hosting where you cannot set headers. But frame-ancestors, report-uri, report-to and sandbox are ignored in a meta tag, and the policy only applies to content after the tag - so it must be the first thing in <head>.

Do I still need X-Frame-Options?

frame-ancestors 'none' replaces it and is more expressive. Keeping X-Frame-Options costs one line and covers browsers that never implemented frame-ancestors, which by now is a very small share. If both are present, browsers that understand CSP use frame-ancestors and ignore the older header.

What breaks first when I enable a CSP?

Inline scripts and styles, then third-party embeds - analytics, chat widgets, maps, YouTube - and then fonts loaded from a different origin than the stylesheet that asks for them. The browser console names the blocked resource and the directive that blocked it, which makes the fix mechanical once you are looking at it.

Can a Content-Security-Policy hurt my search rankings?

Only by breaking your own page. A crawler that renders JavaScript uses a browser engine, so a policy that blocks your bundle for a visitor blocks it for that renderer too, and the indexed version of the page is the broken one. A correct policy is invisible in search. The safe order is report-only first, then enforce, then check the rendered HTML of a real URL in Search Console rather than assuming.

Do I need a nonce if I have no inline scripts?

No, and you are in the better position. script-src 'self' is stronger than any nonce setup because there is nothing to get wrong per response, and no risk of a cached page carrying a stale nonce. Nonces exist for the sites that cannot remove their inline blocks; they are not an upgrade over having none.

What does 'strict-dynamic' actually change?

It says: trust scripts that a script I already trusted chose to load, and ignore my host allowlist. That solves the real problem with allowlists, which is that a vendor script loads three more from hosts you never listed and cannot predict. The trade is that it delegates trust: whatever your nonced script loads is allowed, so a compromised vendor is a compromised page.

Is there a size limit, and does a long policy slow anything down?

No specified limit, but every response carries the bytes, and some proxies and servers cap individual header size around 8 KB. A policy that long is a symptom rather than a performance problem: it usually means host allowlists have been growing for a year, which is the case 'strict-dynamic' exists to fix.

Should I put the policy in report-only forever?

No. Report-only blocks nothing, so it protects nothing - it is a measuring instrument, not a control. Give it a fixed window, a week or two of real traffic, fix what it finds, then enforce. Sites that never make that switch end up with the maintenance cost of a CSP and none of the benefit.

Primary sources this page is checked against

Everything on this page is checked against these. Where they disagree with a blog post, including this one, they win.

  • W3C: Content Security Policy Level 3 — The current specification. Defines the fallback chains, nonces, strict-dynamic and the directives a meta tag must ignore.
  • MDN: Content-Security-Policy — Per-directive reference with the browser support tables, which is where the "verified in" claims above come from.
  • Google web.dev: Strict CSP — The nonce plus 'strict-dynamic' pattern used in the checkout example, including why the backwards-compatibility tokens belong in it.
  • W3C: Reporting API — What report-to and the Reporting-Endpoints header do, and how the newer report body differs from the classic csp-report payload.

The policy is assembled in this tab. The domains you allow amount to a list of every third party your site talks to, and that list stays with you.