What is the Content-Security-Policy header?
The Content-Security-Policy (CSP) response header declares a policy that the browser enforces on the document or worker created from the response: which origins scripts, styles, images and connections may come from, whether inline script may run, where forms may submit and which pages may embed it in a frame. It is defined in the W3C Content Security Policy Level 3 specification, still a Working Draft, and is registered as a permanent field in the IANA HTTP Field Name Registry.
A policy is a semicolon-separated list of directives, each a name followed by a source list. The ones that matter for an API:
| Directive | Effect |
|---|---|
default-src |
The fallback for every fetch directive (script-src, style-src, img-src, connect-src and so on) that is not set explicitly. 'none' means nothing may be loaded. |
frame-ancestors |
Which origins may embed the response in a frame. 'none' forbids all framing. It does not fall back to default-src. |
sandbox |
Applies iframe-style sandbox restrictions to the document, blocking script, forms and more. |
report-to |
Where to send violation reports. Replaces the deprecated report-uri. |
Browsers ignore CSP for non-document requests: a fetch() from your single-page app is governed by CORS, not by the CSP on the API response. The policy only takes effect when a browser renders the response itself, for example when a user opens an API URL directly, a response is loaded in a frame, or an attacker tricks a browser into treating your response as HTML.
API Usage & Best Practices
- For JSON APIs, send the restrictive policy.
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'costs one constant header and has nothing to break: a JSON response loads no scripts, styles or images, and nobody legitimately frames it. Set it once at the gateway or framework level for every API response, including errors. - Include
frame-ancestorsexplicitly. CSP Level 3 spells this out: a policy ofdefault-src 'none'“will still allow the resource to be embedded by anyone.”frame-ancestors 'none'is what blocks clickjacking-style framing of your responses, and it supersedesX-Frame-Optionsin browsers that support both. - Know where it actually helps. The real protection is for the response that ends up rendered as HTML: an error page from your framework that reflects a path or parameter, a misconfigured route that returns
text/html, a user-uploaded file served back.default-src 'none'stops script on such a page from running inline or loading anything, which turns a reflected XSS into a dead end. Pair it with a correctContent-TypeandX-Content-Type-Options: nosniff. - It does nothing for machine clients. SDKs,
curland server-to-server callers ignore it completely. That is not a reason to omit it; it is a reason not to count it as access control. - APIs that serve HTML need a real policy. OAuth consent screens, hosted docs (Swagger UI, Redoc), admin consoles and webhook test pages are documents. Give them a policy written for what they load, typically with
script-srcand nonces or hashes, and keep the blanket'none'policy for the JSON routes. Do not loosen the API-wide header to accommodate one HTML page. - Multiple policies all apply. If a gateway and the application both send
Content-Security-Policy, the browser enforces each one, so the effective policy is the stricter combination. Adding a second header can only tighten things, never loosen them. - Roll out with Report-Only on HTML.
Content-Security-Policy-Report-Onlyreports violations without blocking. It is worth using on documents with a complex policy. For the JSON'none'policy there is nothing to test. - Some directives only work as a header.
frame-ancestors,sandboxandreport-uriare ignored in a<meta>element, so an API must send them in the response header.
Examples
The baseline for a JSON API response:
HTTP/1.1 200 OKContent-Type: application/jsonContent-Security-Policy: default-src 'none'; frame-ancestors 'none'X-Content-Type-Options: nosniffThe same baseline on an error, which is where reflected input most often ends up rendered:
HTTP/1.1 404 Not FoundContent-Type: application/problem+jsonContent-Security-Policy: default-src 'none'; frame-ancestors 'none'An HTML documentation page served by the same API, with its own policy:
HTTP/1.1 200 OKContent-Type: text/html; charset=utf-8Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'; form-action 'self'