Outrings
Security and email authentication

What is a Content Security Policy?

A list of where your page is allowed to load things from. The strongest defence against script injection, and the easiest header to get wrong.

3 min read
Short answer

CSP is a header declaring which sources the browser may load scripts, styles, images and frames from. Anything from an undeclared source is blocked. It is the most effective single defence against cross-site scripting, and it requires knowing what your site actually loads.

The problem it solves

If an attacker gets a script onto your page — through a comment field, a compromised dependency, a vulnerable plugin — the browser runs it with full access to your page, your cookies and your users' sessions. CSP means the browser refuses to run it because it did not come from an approved origin.

It is defence in depth. You should still sanitise input; CSP is what limits the damage when something gets through anyway.

How it is written

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  frame-ancestors 'self';
  object-src 'none';
  base-uri 'self'
DirectiveControls
default-srcThe fallback for anything not otherwise specified.
script-srcWhere JavaScript may load from. The most important one.
style-srcStylesheets.
img-srcImages.
connect-srcWhere fetch, XHR and WebSocket connections may go.
frame-ancestorsWho may embed your page. Replaces X-Frame-Options.
object-srcPlugins. Set to 'none' — nothing legitimate needs it.
base-uriPrevents an injected <base> tag redirecting every relative URL.

The directives that quietly defeat it

  • 'unsafe-inline' in script-src. Allows any inline script, which is exactly what injection produces. This single value removes most of the protection. Use nonces or hashes instead.
  • 'unsafe-eval'. Permits eval() and string-to-code conversion. Some older libraries need it; treat it as a debt to remove.
  • * as a source. Allows everything, which is the same as having no policy for that directive.
  • Broad CDN wildcards. Allowing an entire public CDN means allowing anything anyone has ever uploaded to it.
'unsafe-inline' in style-src is a much smaller risk than in script-src, and is a common pragmatic compromise on sites with inline styles. The one that matters is scripts.

Rolling it out without breaking things

  1. Deploy Content-Security-Policy-Report-Only with your intended policy and a report-uri. Nothing is blocked; violations are reported.
  2. Collect for a week. You will find third-party scripts nobody remembered, an analytics tag added by marketing, and a font from a CDN.
  3. Widen the policy to cover the legitimate ones and remove the rest.
  4. Switch to enforcing mode.
  5. Keep the report endpoint. It becomes an alert when someone injects something, which is the point.

Using nonces

To keep inline scripts without 'unsafe-inline', generate a random nonce per response:

Content-Security-Policy: script-src 'nonce-r4nd0mV4lu3' 'strict-dynamic'

<script nonce="r4nd0mV4lu3">…</script>

The nonce must be unpredictable and different on every response. A static one is worse than useless, because it looks like protection and provides none.

What our audit reports about this

Every item below is measured directly, not inferred. Run it against your own site and the result names the exact rule or header responsible.

  • Whether a CSP is present, and whether it is enforcing or report-only.
  • Which directives are declared and which fall back to default-src.
  • Whether unsafe-inline, unsafe-eval or wildcard sources weaken the policy.
  • Whether frame-ancestors, object-src and base-uri are set.

For agents and scripts, the same measurement is at /api/v1/security?url=yoursite.com — see the API documentation.

Related questions

Is CSP hard to implement?

On a simple site, an afternoon. On a large site with many third-party integrations, a project. Report-only mode is what makes it tractable — it turns guesswork into a list.

Do I need CSP if I have no user input?

It still helps. It limits the damage from a compromised dependency, a hijacked CDN or a malicious browser extension injecting into your page. Those are not hypothetical.

What is strict-dynamic?

It lets a script you explicitly trusted load further scripts, without you enumerating every one. It makes nonce-based policies practical for sites with dynamic script loading.

Read next

All 50 guides · How every check works · API for agents