---
url: https://findutils.com/guides/csp-header-generator
title: "How to Write a Content-Security-Policy Header, Step by Step"
description: "Write a CSP that works: start in Report-Only, read the reports, tighten, then enforce. What each directive does, why 'unsafe-inline' fails, and header vs meta."
category: security
content_type: guide
guide_type: subtopic
cluster: security
locale: en
read_time: 8
status: published
author: "olgunozoktas"
published_at: 2026-09-27T12:00:00Z
excerpt: "A Content-Security-Policy that blocks on day one breaks the site; one that allows 'unsafe-inline' protects very little. Here is the path in between: report first, tighten, then enforce."
tag_ids: ["security", "csp", "http-headers", "web-security", "xss"]
tags: ["Security", "CSP", "HTTP Headers", "Web Security", "XSS"]
primary_keyword: "content security policy generator"
secondary_keywords: ["how to write a csp header", "csp report-only", "script-src unsafe-inline", "csp nonce vs hash", "csp meta tag vs header", "csp google analytics"]
tool_tag: "csp-header-generator"
related_tool: "csp-header-generator"
related_tools: ["security-headers-analyzer", "cookie-analyzer", "url-safety-checker", "data-sanitizer", "meta-tag-generator"]
og_image: "/images/content/guides/csp-header-generator-cover-20260927.webp"
image_alt: "A clear acrylic shield standing like a gate with an arched opening, with teal and white cubes lined up passing through it."
updated_at: "2026-09-27T12:00:00Z"
---

A Content-Security-Policy (CSP) is an HTTP response header that tells the browser which origins may supply scripts, styles, images, frames and connections for a page. The safe way to write one is to send it as `Content-Security-Policy-Report-Only` first, read the violation reports, tighten the source lists, and only then switch to the enforcing `Content-Security-Policy` header. The FindUtils [CSP Header Generator](/security/csp-header-generator/) builds the header line, the matching `<meta>` tag and a list of warnings from presets and per-directive source lists, in your browser.

This guide walks through that path, explains what the common directives control, and covers the two places people most often get CSP wrong: `'unsafe-inline'` in `script-src`, and delivering the policy in a `<meta>` tag.

## What Does a Content-Security-Policy Header Look Like?

A CSP header is a list of **directives**, separated by semicolons, each followed by the **sources** it allows. This is the generator's Strict preset:

```http
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; upgrade-insecure-requests
```

Keywords such as `'self'` and `'none'` must be in single quotes; hosts and schemes (`https://cdn.example.com`, `data:`) must not. A missing quote is not an error the browser reports. The browser reads `self` as a host name and quietly matches nothing. The generator quotes bare keywords for you and refuses a source that is not valid CSP syntax, such as `https//cdn.example.com` or a host with a space in it.

## Step 1: Start in Report-Only Mode

`Content-Security-Policy-Report-Only` is a header that makes the browser check every resource against the policy and report violations without blocking anything. MDN describes it as the way to experiment with a policy by monitoring its effects rather than enforcing it. An enforced policy that misses one CDN host breaks that part of the site for every visitor; a Report-Only policy breaks nothing.

In the generator, pick a preset, switch on **Report-Only mode**, and add a `report-uri` if you collect reports:

```http
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; report-uri /csp-report
```

Two things change in Report-Only mode. The generator leaves `upgrade-insecure-requests` out of the header, and it produces no `<meta>` tag, because the CSP specification does not support the Report-Only header inside a `<meta>` element.

On `report-uri`: CSP Level 3 deprecates it in favour of `report-to`, which names an endpoint declared in a separate `Reporting-Endpoints` header. The generator writes `report-uri`. If you also want `report-to`, add it and its endpoint header yourself.

## Step 2: Read the Reports

Each violation is sent as a JSON POST to your `report-uri`. A report for a script from a host you forgot to list looks like this (fields per MDN's description of the `report-uri` format; values are illustrative):

```json
{
  "csp-report": {
    "document-uri": "https://example.com/pricing/",
    "violated-directive": "script-src-elem",
    "effective-directive": "script-src-elem",
    "blocked-uri": "https://cdn.example.net/widget.js",
    "disposition": "report"
  }
}
```

Read `blocked-uri` and `effective-directive` together: this one says the page loads a script from `cdn.example.net` and `script-src` does not allow it. Each report forces a decision. Either the resource is legitimate and its host goes into the directive, or it is not and you remove it from the page. Browser developer tools show the same violations in the console while you browse, which is quicker for a first pass on staging.

Let the Report-Only header run for long enough to cover pages that are rarely visited: checkout, account settings, error pages, anything behind a login.

## Step 3: Tighten, Then Enforce

Add the hosts the reports justified, and nothing broader. `https://cdn.example.net` is better than `https:`, which allows every host on the web over HTTPS. When the reports stop showing legitimate resources, turn Report-Only off in the generator and deploy the same policy under `Content-Security-Policy`.

Keep `report-uri` on the enforcing header too. After enforcement, a report means something was blocked for a real visitor.

## What Does Each Common Directive Control?

The directives below are the ones the generator shows by default. Descriptions follow MDN's CSP reference.

| Directive | Controls |
|---|---|
| `default-src` | Fallback for any **fetch directive** you do not set (scripts, styles, images, fonts, connections, frames, media, objects, workers) |
| `script-src` | JavaScript sources, including inline scripts and event handlers |
| `style-src` | Stylesheet sources, including inline `<style>` and `style` attributes |
| `img-src` | Images and favicons |
| `font-src` | Fonts loaded with `@font-face` |
| `connect-src` | `fetch()`, XHR, WebSocket and EventSource connections |
| `frame-src` | Pages this page may load in `<iframe>` |
| `object-src` | `<object>` and `<embed>` content |
| `frame-ancestors` | Which sites may embed **this** page in a frame (clickjacking) |
| `base-uri` | URLs allowed in a `<base>` element |
| `form-action` | Where forms may submit |

`default-src` only covers fetch directives. `frame-ancestors`, `base-uri` and `form-action` do not fall back to it, so a policy with only `default-src 'self'` leaves all three unrestricted. The generator warns when `base-uri` or `frame-ancestors` is missing, and when `object-src` is not `'none'`.

## Why 'unsafe-inline' in script-src Defeats Most of the Point

The main job of a CSP is to stop injected script. A cross-site scripting bug usually injects **inline** code: a `<script>` block or an `onerror=` attribute written into your HTML. `'unsafe-inline'` in `script-src` tells the browser to run every inline script on the page, and it cannot tell yours from the attacker's. The policy still restricts external script hosts, but the most common injection path stays open. The generator warns about it for that reason.

CSP offers two ways to allow your own inline scripts without allowing everyone's:

- **Nonces.** The server generates a fresh random value for every response, puts it in the header as `'nonce-...'`, and adds the same value to each legitimate `<script nonce="...">`. The CSP Level 3 specification says the nonce should be at least 128 bits and come from a cryptographically secure random number generator. An attacker who injects markup cannot guess the value.
- **Hashes.** `'sha256-...'` (or `sha384`, `sha512`) is the base64 digest of the exact contents of one inline script. It suits static pages, because any change to the script, even whitespace, changes the hash.

```http
Content-Security-Policy: script-src 'self' 'nonce-4AEemGb0xJptoIGFP3Nd' 'unsafe-inline'; object-src 'none'; base-uri 'none'
```

Under CSP Level 2 and later, a browser ignores `'unsafe-inline'` when the same directive also contains a nonce or a hash. That is why the header above is safe in current browsers and still works in very old ones, and why the generator does not warn about `'unsafe-inline'` when a nonce or hash sits beside it. A static generator cannot issue a per-response nonce. That has to happen in your server or framework on every request. The value above is only a placeholder to show the syntax.

`'unsafe-inline'` in `style-src` is a smaller risk, and the Typical preset allows it because many sites and libraries set inline styles.

## Header or meta Tag?

Send the HTTP header whenever you can set response headers. The `<meta http-equiv="Content-Security-Policy">` tag is a fallback for hosts that do not let you, and it is weaker in ways the CSP specification spells out:

| | HTTP header | `<meta>` tag |
|---|---|---|
| `frame-ancestors` | Works | Ignored |
| `report-uri` | Works | Ignored |
| `sandbox` | Works | Ignored |
| Report-Only | `Content-Security-Policy-Report-Only` | Not supported |
| Covers | The whole response | Only content after the tag is parsed |

The generator writes both. It leaves `frame-ancestors` and `report-uri` out of the `<meta>` tag and says which ones it dropped. It does not build a `sandbox` directive at all. If clickjacking protection matters, and for most pages it does, the header is the only option.

## What the Three Presets Allow

- **Strict** allows scripts, styles, images, fonts and connections only from your own origin, sets `default-src 'none'`, and blocks framing with `frame-ancestors 'none'`. It produces no warnings, and it will break any page that loads a third-party script, font or inline style.
- **Typical site** starts from `default-src 'self'` and allows inline styles, images from `data:` or any `https:` host, fonts from `data:` URLs, and framing by your own origin only.
- **With Google Analytics** is Typical plus Google's documented host patterns. It adds `https://*.googletagmanager.com` to `script-src`, `https://*.google-analytics.com` and `https://*.googletagmanager.com` to `img-src`, and those two plus `https://*.analytics.google.com` to `connect-src`. These are the hosts Google lists in its CSP guidance for Google Analytics 4 and Tag Manager.

The Google Analytics preset does **not** allow inline scripts. The standard gtag.js install includes an inline `<script>` that configures `dataLayer`, and this policy blocks it until you add a nonce or hash for it (Google's guidance recommends a nonce). Some Google features, such as Google Signals, need additional hosts that Google's guidance lists separately. Check your reports before you enforce.

## Check the Live Header After You Deploy

The generator checks the policy text you build. It does not fetch your site. After deploying, paste the real response headers into the [Security Headers Analyzer](/network/security-headers-analyzer/) to confirm the header your server actually sends, alongside HSTS and the other security headers. The [security headers guide](/guides/security-headers-analyzer-guide/) covers how to capture them with `curl -I`.

The same generator is available through the FindUtils REST API (`csp-generate`) and as the `csp_generate` MCP tool. Those calls send your directives to FindUtils over TLS to build the policy.

## Related Tools

- [Cookie Analyzer](/security/cookie-analyzer/) checks the `Secure`, `HttpOnly` and `SameSite` flags on cookies from the same response. `HttpOnly` limits what an injected script can read.
- [URL Safety Checker](/security/url-safety-checker/) checks links before you publish them or add their hosts to a policy.
- [Data Sanitizer](/security/data-sanitizer/) cleans and encodes untrusted input, the escaping layer a CSP backs up rather than replaces.
- [Meta Tag Generator](/seo/meta-tag-generator/) writes the other `<head>` tags a page needs.

## FAQ

**Does a CSP replace escaping output?**
No. A CSP limits what an injected script can do and where it can load from. Escaping and sanitizing output is what stops the injection in the first place. Use both.

**Can I combine several CSP headers?**
Yes. When a response carries more than one policy, the browser enforces each of them, so a resource must pass every policy to load. A second header can only make the result stricter.

**Why was my source rejected?**
The generator accepts keywords, nonces, hashes, schemes such as `https:` or `data:`, and hosts with an optional scheme, a leading `*.` wildcard, a port and a path. Quotes, commas, semicolons and angle brackets inside a source are refused, so they never reach the header.
