# Content Security Policy

## What it is

Content Security Policy is a set of rules a website hands to the browser about
what its pages may run, fetch and submit, which the browser then enforces on the
site's behalf. It is almost always shortened to CSP. The site sends the rules in
a `Content-Security-Policy` response header, or writes them into the page in a
`meta` element, and from that moment any script, stylesheet, image, font, frame
or connection the rules do not allow is refused before it loads.

A policy is a short string of directives separated by semicolons. Each directive
names one kind of resource or behaviour and the sources it accepts. Shown one
directive per line for reading, though on the wire it is a single header value,
a strict policy looks like this:

```text
default-src 'self';
script-src 'nonce-q8Zt3VwR1mKx7NpL2cYfJA==' 'strict-dynamic';
object-src 'none';
base-uri 'none';
form-action 'self';
frame-ancestors 'none'
```

The vocabulary is small. `'self'` means the page's own origin. A host such as
`https://static.example.com` or a scheme such as `https:` widens that. `'none'`
allows nothing. A nonce is a random value the server generates for one response
and writes both into the policy and onto each script element it vouches for. A
hash is the digest of an inline script's exact text, so that one script and no
other may run. Two keywords, `'unsafe-inline'` and `'unsafe-eval'`, switch
protections back off, and their names are a fair warning.

`default-src` is the fallback for the fetch directives a policy leaves out, such
as `script-src`, `style-src`, `img-src` and `connect-src`. It is a fallback for
those alone. Directives that govern behaviour rather than fetching, among them
`base-uri`, `form-action` and `frame-ancestors`, stay unrestricted unless they
are written out.

A policy can also run in a watching mode. Sent as
`Content-Security-Policy-Report-Only`, it blocks nothing and instead reports
each violation to an endpoint named with `report-to`, which uses the browser's
Reporting API. The older `report-uri` directive is deprecated in its favour. The
current text of the standard is Content Security Policy Level 3, a W3C Working
Draft from the Web Application Security Working Group.

## Why it matters

The attack CSP was built against is cross-site scripting. Somebody gets text of
their choosing into a page, through a comment field, a search term echoed back
into the HTML or a compromised dependency, and the browser runs it as script
with all the authority the site's own code has: it can read the page, act as the
logged-in user and send what it finds anywhere. The first defence is escaping
output correctly, and that has to be right in every template, on every line,
every time. CSP is the second defence. When an escaping mistake does slip
through, the injected script still has to get past the policy, and a policy that
only runs scripts carrying this response's nonce turns it away.

It also limits what a script that does run can reach. `connect-src` decides
which hosts script may talk to, which narrows where a skimmer can post what it
collects. `frame-ancestors` decides which sites may show the page inside a
frame, the standard defence against clickjacking, where a page is overlaid on
another so that a click lands somewhere the user never meant. `form-action`
decides where forms may submit, and `base-uri` stops an injected `base` element
from quietly redirecting every relative address on the page.

It is worth being exact about who CSP protects. It runs in the browser, so it
guards the person reading the page from content an attacker planted there. It
does nothing for the server against that same person, who can edit or strip the
header in their own browser in seconds. A policy is a reason to trust less of
the page, never a reason to trust more of the request.

The cost is mostly inventory. A policy forces a team to know every origin its
pages load from, every inline script and every event handler written as an
attribute. A new site can be built to a strict policy from its first commit for
very little. An older site with inline handlers, a tag manager and several
widgets pays for the clean-up first, and that clean-up is usually the real
project.

## What it looks like in practice

This site holds itself to one. The [security page](/security/) lists a strict
Content Security Policy, declared in the page itself, with `form-action 'none'`,
because the site posts no forms anywhere. The same page lists one first-party
script of under a kilobyte and no third-party requests at all, and that is what
keeps such a policy short: everything a page needs comes from one host, the
site's own, so there is very little to allow.

An application with logged-in users needs more than a short allowlist. On a
platform holding professional and organisational data,
[the Next.js proxy middleware sets a Content-Security-Policy with a per-request nonce and `strict-dynamic`](/portfolio/hardened-the-application-with-nonce-based-csp-hsts-77/),
alongside HSTS and SameSite cookies. A fresh nonce on every response means that
only the script elements the server stamped on that response will run. With
`strict-dynamic` in the policy, a script the nonce trusts may load further
scripts itself, so the policy does not have to name every host a bundle fetches
its chunks from. That platform works from the assumption that the client is
hostile. Each request's entitlements are therefore checked again by the server,
and the gates in the frontend count for user experience only. CSP is one layer
of several there, protecting users from injected code, and authorisation is
decided elsewhere.

## Where it goes wrong

**The policy goes live and half the site stops working.** A strict policy sent
straight to enforcement blocks the inline script nobody remembered and the
tracking tag someone added two years ago. Ship it as
`Content-Security-Policy-Report-Only` first, read the reports until they settle,
allow or remove each source they name, and only then enforce. The report-only
header has to be a real header: the specification does not support it in a
`meta` element.

**The allowlist trusts a whole host.** `script-src https://cdn.example.com`
allows every file on that host, including old library versions and endpoints
that echo a caller's input back as script. The specification itself says that
deploying a policy that works against cross-site scripting is a challenge, and
recommends what it names a Strict CSP: `script-src` built from nonces or hashes
with `'strict-dynamic'`, plus `base-uri` set to `'self'` or `'none'`.

**`'unsafe-inline'` is assumed to allow every inline script.** Once a nonce or a
hash appears in the same source list, browsers that understand them ignore
`'unsafe-inline'`. The rule lets a policy keep `'unsafe-inline'` as a fallback
for browsers too old to know nonces while current ones apply the strict list,
and it surprises a team that adds a nonce to one script and finds its other
inline scripts blocked.

**The nonce stops being a secret.** The specification requires a new value every
time a policy is sent and asks for at least 128 bits from a cryptographically
secure generator. A nonce hard-coded into a template, or a whole page with its
nonce stored in a shared cache and served to everyone, gives an attacker the
value they need to stamp their own script.

**A policy in a `meta` element does less than the same policy in a header.** The
browser ignores `frame-ancestors`, `report-uri` and `sandbox` when they arrive
that way, and the policy only governs what comes after the element in the
document, so anything above it in the head loads unchecked. A site with no say
over its response headers still gets real protection from a `meta` policy,
placed as early in the head as it will go, but anti-framing needs the header.

**A second policy tightens instead of loosening.** A response can carry several
policies, from a framework, a proxy and a CDN, and a resource has to pass every
one of them. Adding a policy can only restrict, so a permissive header added
downstream to fix a blocked request does nothing, and the fix has to go into the
policy that blocked it.

## Further reading

- [Content Security Policy Level 3](https://www.w3.org/TR/CSP3/) — the standard
  itself, with the Strict CSP recommendation and a section on how nonces get
  stolen.
- [HTML Standard: the meta element](https://html.spec.whatwg.org/multipage/semantics.html)
  — the pragma that applies a policy declared in the page, the directives it
  drops, and why resources fetched before it may slip past.
- [Reporting API](https://www.w3.org/TR/reporting-1/) — the endpoint mechanism
  behind `report-to`, for reading violations before enforcing.
- [Trusted Types](https://www.w3.org/TR/trusted-types/) — the companion standard
  that a `require-trusted-types-for` directive switches on, aimed at script
  injected through the DOM.

<https://engineer.company/glossary/csp/>
