# Content-Security-Policy (CSP): what it is and how to deploy it safely

> Content-Security-Policy tells the browser which scripts, styles and frames a page may load. Learn the key directives, strict nonce policies and a safe rollout.

Author: Joshua Martinelle, Security engineer at Tenable (https://www.websec0.com/guides/authors/joshua-martinelle/)
Canonical: https://www.websec0.com/guides/content-security-policy/
Updated: 2026-10-07

## In short

- `Content-Security-Policy` is an HTTP response header listing the sources a page may load scripts, styles, frames and other resources from. Anything else is blocked by the browser.
- Its main job is to limit cross-site scripting (XSS). With `frame-ancestors` it also replaces `X-Frame-Options` against clickjacking.
- Domain allowlists are easy to bypass. A strict policy based on nonces or hashes, with `'strict-dynamic'`, `object-src 'none'` and `base-uri`, is what holds up.
- Never put `'unsafe-inline'` in `script-src`: it allows exactly the injected scripts CSP is meant to stop, and WebSec0 only gives half the points for it.
- Roll out with `Content-Security-Policy-Report-Only` and a reporting endpoint, fix what it reports, then switch to the enforcing header.

## What is Content-Security-Policy?

**Content-Security-Policy (CSP) is an HTTP response header that tells the
browser which sources a page is allowed to load scripts, styles, images, fonts,
frames and other resources from.** When the page tries to load or run anything
the policy does not allow, the browser blocks it and logs a violation. Its main
purpose is to limit cross-site scripting (XSS): a script injected into your
page does not run if it does not match the policy.

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

```http
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none'; frame-ancestors 'none'
```

This policy loads everything from the page's own origin (`'self'`), also allows
scripts from `cdn.example.com`, forbids plugins, and stops other sites from
framing the page. Inline `<script>` blocks, `onclick` attributes and
`javascript:` URLs are blocked because the policy does not allow
`'unsafe-inline'`.

CSP is a second line of defence. It does not fix the injection bug, but it
decides what the injected code can do. Adoption is still low: in Scott Helme's
June 2026 crawl, 170,057 of the 819,002 top sites that responded sent a
`Content-Security-Policy` header (about 21%), and 46.8% of those policies
contained `'unsafe-inline'`.

## What CSP protects against

| Threat | Directive that stops it | How |
| --- | --- | --- |
| Cross-site scripting (XSS) | `script-src` | Only scripts from allowed sources, or carrying a valid nonce or hash, run. |
| Clickjacking | `frame-ancestors` | Decides which sites may embed your page in a frame. |
| `<base>` tag injection | `base-uri` | Stops an attacker from repointing every relative URL on the page. |
| Form hijacking | `form-action` | Limits where forms can submit, including injected ones. |
| Plugin-based script execution | `object-src` | `'none'` disables `<object>` and `<embed>`. |
| Mixed content | `upgrade-insecure-requests` | Rewrites `http://` subresource URLs to `https://`. |
| DOM-based XSS | `require-trusted-types-for` | Makes sinks such as `innerHTML` reject plain strings. |

## The directives that matter

Fetch directives control where each type of resource can come from. When one is
missing, the browser falls back to `default-src`.

| Directive | Controls | Typical value |
| --- | --- | --- |
| `default-src` | Fallback for every fetch directive not set | `'self'` |
| `script-src` | JavaScript: files, inline blocks, event handlers, `eval()` | `'nonce-…' 'strict-dynamic'` or `'self'` |
| `style-src` | Stylesheets and inline styles | `'self'` |
| `img-src` | Images and favicons | `'self' data:` |
| `connect-src` | `fetch()`, XHR, WebSocket and `EventSource` | `'self'` plus your API origins |
| `font-src` | Web fonts | `'self'` |
| `frame-src` | Pages you embed in frames | `'none'` or the embed origins |
| `object-src` | `<object>` and `<embed>` | `'none'` |

These directives are **not** covered by `default-src`, so set them explicitly:

| Directive | Controls | Typical value |
| --- | --- | --- |
| `base-uri` | URLs allowed in `<base href>` | `'none'` or `'self'` |
| `form-action` | Where forms may submit | `'self'` |
| `frame-ancestors` | Who may frame your page | `'none'` or `'self'` |
| `upgrade-insecure-requests` | Upgrades `http://` subresources | No value |
| `report-to` | Endpoint that receives violation reports | An endpoint name |

Source values are matched exactly. `'self'` means the page's own scheme, host
and port, not its subdomains. Keywords such as `'self'`, `'none'` and
`'unsafe-inline'` keep their single quotes; host names do not.

## Allowlists versus strict policies

The first instinct is to list every domain your scripts come from. That is
called an allowlist policy, and it is the weakest way to use CSP.

Google researchers analysed policies from 1.68 million hosts in 2016. They
found that 94.72% of distinct policies could be bypassed, and that 14 of the 15
domains most often allowed for scripts hosted endpoints an attacker could abuse,
such as JSONP callbacks or old AngularJS copies. If a domain on your list serves
anything an attacker can steer, they load their payload from it and your policy
waves it through.

A **strict policy** trusts individual scripts instead of whole domains:

- **Nonces.** The server generates a fresh random value for each response
  (at least 128 bits), puts it in the header as `'nonce-<value>'` and on each
  legitimate `<script nonce="<value>">`. Injected scripts cannot guess it.
  This suits pages rendered by an application.
- **Hashes.** The header lists the SHA-256, SHA-384 or SHA-512 hash of each
  inline script, such as `'sha256-…'`. Only those exact scripts run. This suits
  static sites, where the content does not change per request.
- **`'strict-dynamic'`.** Scripts that were trusted by nonce or hash may load
  further scripts, so you do not have to list every bundle a tag manager or
  framework pulls in. In browsers that support it, allowlisted hosts and
  `'self'` in `script-src` are ignored.

The policy recommended by Google's security team, and our default for
applications:

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

You will often see `https: 'unsafe-inline'` appended as a fallback for old
browsers. Browsers that understand nonces ignore those two values, which is
every browser with CSP Level 2 support. We recommend leaving them out: WebSec0
reads `'unsafe-inline'` in `script-src` as a weakness, nonce or not.

Since early 2026, Trusted Types work in all major browsers. Adding
`require-trusted-types-for 'script'` to a strict policy also blocks DOM-based
XSS, where your own JavaScript writes attacker-controlled strings into
`innerHTML` and similar sinks. It needs code changes, so test it in report-only
mode first.

## Three policies to start from

Pick the one closest to your site, then adjust the sources to what you actually
load.

**A static site or documentation** with no inline scripts (or with hashes for
the few it has):

```http
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
```

**A server-rendered application** that can add a nonce to each response.
Replace `{RANDOM}` with a new value for every response, and never cache a page
together with its nonce:

```http
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'
```

**A JSON API or anything that never renders HTML:**

```http
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
```

## Roll it out without breaking your site

A policy that is too strict breaks scripts, fonts or analytics the moment it is
enforced. Test it first:

1. **Inventory what you load.** Open your main pages with the browser's
   network panel and note every script, style, font, image and API origin,
   plus any inline `<script>` blocks and `on*` attributes.
2. **Ship it in report-only mode.** Send the policy as
   `Content-Security-Policy-Report-Only`. The browser blocks nothing and reports
   every violation to the console and to your endpoint:

   ```http
   Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
   Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; report-to csp-endpoint; report-uri https://example.com/csp-reports
   ```

   `report-to` works in all major browsers since 2026; `report-uri` covers older
   ones and is ignored by browsers that understand `report-to`.
3. **Fix the violations.** Move inline scripts and event handlers into files,
   add nonces or hashes to the inline scripts you keep, and allow the specific
   origins you rely on rather than broad schemes like `https:`.
4. **Leave it running for a while.** A week of real traffic catches pages,
   browser extensions and third-party scripts that a manual test misses.
   Extension noise (`chrome-extension://`, `moz-extension://`) can be ignored.
5. **Enforce it.** Rename the header to `Content-Security-Policy`. You can keep
   a stricter candidate running in report-only mode next to it: the browser
   enforces one and reports on the other.
6. **Check every response.** Error pages, redirects and API routes need the
   header too. Scan the site with WebSec0 to confirm it is served and graded.

## Common mistakes

- **`'unsafe-inline'` in `script-src`.** It re-allows inline scripts and
  `onerror=` handlers, the most common XSS payloads. Use nonces or hashes.
- **`'unsafe-eval'`.** It allows `eval()`, `new Function()` and string
  timers. Most modern libraries no longer need it.
- **Wildcards and schemes.** `*`, `https:` or `data:` in `script-src` allow
  scripts from almost anywhere.
- **Allowing a whole CDN.** `https://cdn.jsdelivr.net` or
  `https://unpkg.com` serves any package anyone has published, including an
  attacker's. Self-host the file or use a nonce.
- **Forgetting the directives `default-src` does not cover.** Without
  `base-uri`, `form-action` and `frame-ancestors`, those protections are off.
- **Only setting it on the home page.** Error pages, login flows and older
  sections are often served by a different rule or application.
- **Reusing or caching nonces.** A nonce that is the same on every response, or
  frozen in a CDN cache, gives an attacker the key.
- **Relying on a `<meta>` tag.** It cannot set `frame-ancestors`, cannot
  report, and only covers what is parsed after it.

## How WebSec0 grades Content-Security-Policy

CSP is the heaviest header in the WebSec0 headers grade: **25 of the 100
points**. The scanner reads the `Content-Security-Policy` response header of
your page and scores it like this:

| What WebSec0 sees | Status | Points |
| --- | --- | --- |
| No `Content-Security-Policy` header | Fail | 0 / 25 |
| `'unsafe-inline'` in `script-src` | Warning | 12 / 25 |
| No `script-src`, and `'unsafe-inline'` in `default-src` | Warning | 12 / 25 |
| Any other policy | Pass | 25 / 25 |

A few details follow from how the check works:

- `Content-Security-Policy-Report-Only` and `<meta>` policies are not graded.
  Report-only is a testing mode, not protection.
- When the response carries several CSP headers, the first one is graded.
- A `frame-ancestors` directive also makes the X-Frame-Options check pass,
  worth another 15 points, even if you do not send `X-Frame-Options`.
- The grade does not yet look at `'unsafe-eval'`, wildcards or bypassable
  allowlists. A passing grade is a floor, not proof the policy is strict:
  aim for the strict policy above.

## Frequently asked questions

### What is a Content-Security-Policy header?

It is an HTTP response header that tells the browser which origins a page may load scripts, styles, images, fonts, frames and other resources from, and where it may be framed or submit forms. Resources that do not match the policy are blocked and reported.

### Does CSP replace input validation and output encoding?

No. CSP is a second layer that limits what an injected script can do when the first layer fails. You still need to escape output and validate input. A strict CSP makes most remaining XSS bugs unexploitable, which is why it is worth deploying.

### Should I set CSP in a meta tag or an HTTP header?

Use the HTTP header. A `<meta http-equiv>` policy only applies to content parsed after it, cannot use `frame-ancestors`, `sandbox` or reporting, and cannot be report-only. WebSec0 reads response headers, so a meta policy is not graded.

### Is 'unsafe-inline' in style-src a problem?

It is much less dangerous than in `script-src`, and WebSec0 does not penalise it. Injected CSS can still deface a page or leak data through attribute selectors, so move styles to files or use nonces when you can.

### Can I send more than one Content-Security-Policy header?

Yes. The browser enforces every policy it receives and a resource must pass all of them, so a second policy can only make things stricter. Note that WebSec0 grades the first `Content-Security-Policy` header in the response.

### What is the difference between report-uri and report-to?

`report-uri` posts each violation to a URL and is the legacy mechanism. `report-to` names an endpoint declared in the `Reporting-Endpoints` header and uses the Reporting API; it has worked in all major browsers since 2026. Browsers that support `report-to` ignore `report-uri`, so sending both is safe.

### Does CSP replace X-Frame-Options?

For current browsers, yes: `frame-ancestors` supersedes `X-Frame-Options` when both are present. WebSec0 counts the X-Frame-Options check as passed when your CSP contains `frame-ancestors`, even without the older header.

## Sources

- [Content Security Policy Level 3](https://www.w3.org/TR/CSP3/), W3C
- [Content Security Policy (CSP) guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP), MDN Web Docs
- [CSP: report-to directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/report-to), MDN Web Docs
- [CSP: require-trusted-types-for directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for), MDN Web Docs
- [CSP Is Dead, Long Live CSP! On the Insecurity of Whitelists and the Future of Content Security Policy](https://research.google/pubs/pub45542/), Weichselbaum, Spagnuolo, Lekies and Janc, ACM CCS 2016
- [Mitigate cross-site scripting (XSS) with a strict Content Security Policy](https://web.dev/articles/strict-csp), web.dev
- [Content Security Policy Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html), OWASP
- [Top 1 Million analysis, June 2026: ten years of web security](https://scotthelme.co.uk/top-1-million-analysis-june-2026-ten-years-of-web-security/), Scott Helme
