Content Security Policy
Prerequisites: Link and embed security
A Content Security Policy (CSP) tells the browser which sources of scripts, styles, images, and other resources a page may use. If an attacker manages to inject markup (XSS), a strict CSP stops their script from running. It's defense in depth: CSP doesn't replace proper escaping, it limits the damage when escaping fails.
Delivering a policy
Preferably as an HTTP header:
Content-Security-Policy: default-src 'self'; img-src 'self' https://images.example; object-src 'none'; base-uri 'none'; frame-ancestors 'self'
Or with a meta tag in the head (which doesn't support
frame-ancestors, report-uri, or sandbox):
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none'; base-uri 'none'">
Common directives
| Directive | Controls |
|---|---|
default-src |
Fallback for the *-src directives not listed |
script-src |
JavaScript |
style-src |
CSS |
img-src, font-src, media-src |
Images, fonts, audio/video |
connect-src |
fetch, XHR, WebSocket destinations |
frame-src |
What you may embed in iframes |
frame-ancestors |
Who may embed you |
form-action |
Where forms may submit |
base-uri |
Allowed base URLs |
object-src |
Plugins — set to 'none' |
Source values include 'self', 'none', specific origins
(https://cdn.example), https:, 'nonce-…', 'sha256-…',
'strict-dynamic', and — to avoid — 'unsafe-inline' and
'unsafe-eval'.
A nonce-based strict policy
The recommended approach: allow scripts only if they carry a random, per-response nonce. Injected scripts don't know it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-R4nd0mV4lu3'; object-src 'none'; base-uri 'none'">
<title>CSP demo</title>
<script nonce="R4nd0mV4lu3">
document.addEventListener("securitypolicyviolation", e =>
console.log("blocked:", e.violatedDirective, e.blockedURI));
</script>
</head>
<body>
<script nonce="R4nd0mV4lu3">console.log("script with the nonce runs");</script>
<!-- Wrong: no nonce, so the policy blocks these two -->
<script>console.log("injected script runs");</script>
<button type="button" onclick="console.log('inline handler runs')">Click</button>
<script nonce="R4nd0mV4lu3">document.querySelector("button").click();</script>
</body>
</html>
script with the nonce runs
blocked: script-src-elem inline
blocked: script-src-attr inline
The script with the nonce ran; the script without it and the inline
onclick handler were blocked (the browser also prints its own
"Refused to execute..." errors in the console).
- The server must generate a new unpredictable nonce for every response — a fixed nonce like the one in this demo is useless.
- Move
onclick="..."handlers into scripts withaddEventListener, andjavascript:URLs into real buttons. 'strict-dynamic'lets a trusted (nonce'd) script load further scripts — useful for loaders and tag managers.
Rolling out safely
Start with Content-Security-Policy-Report-Only: the browser reports
violations (to a report-to endpoint) without blocking anything. Fix
what breaks, then switch to enforcing.
Trusted Types
require-trusted-types-for 'script' makes the browser reject plain
strings passed to dangerous sinks like innerHTML — only values created
by a policy you define are accepted. It's supported in Chromium-based
browsers, Safari 26, and Firefox 148.
Other useful security headers alongside CSP:
Strict-Transport-Security, X-Content-Type-Options: nosniff, and
Referrer-Policy.