---
title: "Set a content security policy"
audience: "anyone adding a Content-Security-Policy to a site"
description: "You'll write a Content-Security-Policy, find out what it blocks before it blocks anything, read each violation in the dev panel and see why nonces don't work…"
snippet_caption: "config/app.toml"
snippet: "[document.csp]\ndefault-src = [\"'self'\"]\nscript-src = [\"'self'\"]\nstyle-src = [\"'self'\", \"'unsafe-inline'\"]\nimg-src = [\"'self'\", \"data:\"]\nbase-uri = [\"'none'\"]\nobject-src = [\"'none'\"]\nframe-ancestors = [\"'self'\"]"
---

import { Install } from "@src/ui/Install";

You'll write a Content-Security-Policy, find out what it blocks before it blocks anything, read each violation in the dev panel and see why nonces don't work for inline scripts here.

<Install captured="0.x" />

## Write the policy as directives

```toml title="config/app.toml"
[document.csp]
default-src = ["'self'"]
script-src = ["'self'"]
style-src = ["'self'", "'unsafe-inline'"]
img-src = ["'self'", "data:"]
base-uri = ["'none'"]
object-src = ["'none'"]
frame-ancestors = ["'self'"]
```

Each directive is a list of sources and the host composes the header from them.

> A policy governs every script on the page, so a tag that loaded fine yesterday stops the moment a policy exists. Run it in report-only mode first, as the next section shows.

## Learn what it would block first

An application `fsr new` made already starts here. Its `config/app.toml` carries:

```toml title="config/app.toml"
# Reported, not enforced. Rename the table to [document.csp] to enforce it.
[document.csp_report_only]
default-src = ["'self'"]
script-src = ["'self'"]
base-uri = ["'none'"]
object-src = ["'none'"]
frame-ancestors = ["'self'"]
```

Add an origin the same way you would to an enforced policy:

```toml title="config/app.toml" {3}
[document.csp_report_only]
default-src = ["'self'"]
script-src = ["'self'", "https://www.googletagmanager.com"]
```

It takes the same directives and is sent as `Content-Security-Policy-Report-Only`. The browser reports violations and enforces nothing. Both keys may be set at once, so a deployment can enforce the policy it is sure of and report against the one it wants next.

Use this for anything whose origins you do not control. An allowlist covers analytics, whose origins are stable. It cannot cover an ad network, which injects scripts from origins that change per impression.

## Read violations in the dev panel

Under `fsr dev`, every violation the page meets shows up in a panel at the top right of the page, with the change that would admit it:

```
fsr dev: the content security policy would block 2 things

reported an inline script (script-src-elem)
  move it into a file the page loads or add its hash to script-src in [document.csp_report_only]

reported https://example.com (img-src)
  add img-src = ["'self'", "https://example.com"] to [document.csp_report_only], which carries over what default-src allowed
```

`reported` means a report-only policy let it through. Under `[document.csp]` the same lines say `blocked`. The second suggestion lists `'self'` because this policy declares no `img-src`: declaring one replaces `default-src` for images, so it has to carry `default-src`'s sources over or same-origin images stop loading. Click the panel to hide it.

## The import map's hash

The document carries an inline import map, so `script-src` gets a sha256 of it:

```
script-src 'self' 'sha256-svThEqLXIp8mujEwI5OYbQV5w06ndiss2LoQLZGpW0Q='
```

The host computes it from the text it actually emits.

**A hash in `script-src` makes `'unsafe-inline'` ignored.** The spec requires this, so you cannot add `'unsafe-inline'` to permit some other inline script while a hash is present. The browser ignores it.

`fsr build` and `fsr dev` print a `csp` row when a policy falls into this, so you see it on the build that introduced it:

```
csp       `document.csp` names `'unsafe-inline'` in `script-src` while the document carries an import map
```

**The hash has to be taken from the emitted text.** Any rewrite of the import map has to happen before the policy reads it. Get that order wrong and the header carries a hash of a document that no longer exists, so the browser blocks the import map.

## Nonces

A nonce is the usual answer to inline scripts, but it does not work here.

A nonce must be unique per response. A prerendered document is a file written once and served to everybody, so its nonce is a constant that everybody's browser sees, which defeats the purpose. The render memo has the same problem: a memoized subtree is rendered once and replayed, nonce included.

So the host issues nonces under `dev`, where neither prerendering nor the memo is in play. Production uses hashes. If you want a nonce in production, move that script out of the document so it is no longer inline.

## Extension violations in the console

Turn a policy on, open the site in your own browser and the console fills with violations you did not cause:

```
CSP: blocked an inline script (script-src-elem) ... at inpage.js:1:358533
CSP: blocked an inline script (script-src-elem) ... at 681de782-e99b-49c9-b28e-02e6e80251e1:19:51
```

Those are a browser extension injecting into your page, with the policy refusing. MetaMask does this and so does any extension that injects scripts into the document. You cannot allow them while a hash is present, per the rule above. Blocking them only affects the extension.

Check where a violation comes from before acting on it. A violation at `moz-extension://...` or a UUID-named document is not your code. The dev panel counts these on a line of their own rather than listing them. Neither are the `MaxListenersExceededWarning` and orphaned-stream lines that come with it.

## Check it in the production shape

A policy is a header, so check it in the shape that sends it. Run the deploy tree behind whatever will sit in front of it in production and read the console on every route, the way [440](https://snapfirers.com/fsr/learn/440-find-broken-links) does. A CSP violation is a console error, so the same sweep catches it.

> Watch for a page that renders but does not respond, because the module that hydrates it was blocked. The page looks right, so it can look like a CSS or event-handler bug.

Next up: [500. Move a loader into Rust](https://snapfirers.com/fsr/learn/500-move-a-loader-into-rust).
