What CSP does and does not do
Content-Security-Policy is a browser-enforced allowlist that tells the page which sources scripts, styles, images, fonts, frames, and network requests are permitted to come from. When the page tries to load or execute something outside that allowlist, the browser blocks it. That is the entire mechanism: it constrains where resources are allowed to originate.
The important framing is that CSP limits the blast radius of cross-site scripting (XSS); it does not prevent XSS. If an attacker injects markup into your page, a good policy can stop the injected <script> from running, stop an inline event handler from firing, and stop stolen data from being POSTed to an attacker-controlled host. But the underlying injection flaw is still there. CSP is a second layer behind output encoding and input handling, not a substitute for them. Treat it as damage control for the day your real defenses fail.
CSP can be delivered two ways, and they are not equivalent. The Content-Security-Policy HTTP response header is the full-strength version. A <meta http-equiv="Content-Security-Policy"> tag works for most fetch-directives but silently ignores several directives that only function over a header. In particular, frame-ancestors (clickjacking protection), report-uri/report-to (violation reporting), and sandbox cannot be set via meta. If you care about clickjacking or monitoring, you must use the header.
The checklist
Work through these in order. Each one closes a specific class of abuse.
- Start from
default-src 'none'. Deny everything by default, then add back only the resource types you actually serve. A deny-by-default baseline means a directive you forgot fails closed instead of open. - Set
script-srcwithout'unsafe-inline'and without'unsafe-eval'. This is the single most important directive. Drive inline scripts with per-response nonces or'sha256-...'hashes, and remove any reliance oneval,new Function, or string-argumentsetTimeout. - Have a
style-srcstrategy. Inline styles are far lower risk than inline scripts, but'unsafe-inline'on styles still enables some data-exfiltration and UI-redress tricks. Prefer external stylesheets or nonced/hashed style blocks; only fall back to'unsafe-inline'for styles if a dependency forces it. - Set
object-src 'none'. Plugins (<object>,<embed>) are a legacy script-execution vector almost nobody needs. Turn them off outright. - Set
base-uri 'none'or'self'. Without this, an injected<base>tag can rewrite where every relative URL (including scripts) resolves, defeating an otherwise tightscript-src. - Set
frame-ancestors 'none'(or a strict allowlist). This is your real clickjacking control. It supersedesX-Frame-Optionsand supports multiple origins, which the older header cannot. - Set
form-action 'self'. Stops an injected form from submitting credentials or tokens to an external endpoint. - Lock
connect-srcto your API origins. This governsfetch,XMLHttpRequest, WebSocket, andsendBeacon. A tightconnect-srcis what actually stops fetch-based exfiltration of stolen data even if a script runs. - Add
upgrade-insecure-requests. Transparently upgrades strayhttp://subresource URLs to HTTPS so a single mixed-content reference does not break the page or leak over plaintext. - Wire up
report-to(and legacyreport-uri) for monitoring. You cannot tune a policy you cannot see. Reporting is how you learn what the policy breaks before and after enforcement.
A solid starter policy
This is a reasonably strict policy for a typical app that serves its own scripts and styles and talks to its own API. Adjust the hosts; the structure is the point.
Content-Security-Policy: default-src 'none'; script-src 'self' 'nonce-r4nd0m'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://api.example.com; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; upgrade-insecure-requests; report-to csp-endpoint;
Line by line:
default-src 'none'- deny every resource type unless a more specific directive allows it.script-src 'self' 'nonce-r4nd0m'- scripts may load from your own origin, plus inline scripts carrying the matching per-response nonce.style-src 'self'- stylesheets only from your origin; no inline styles.img-src 'self' data:- images from your origin plus small inlinedata:images (dropdata:if you do not need it).font-src 'self'- fonts only from your origin.connect-src 'self' https://api.example.com- network calls only to your origin and your API.base-uri 'none'- no<base>tag may change relative URL resolution.form-action 'self'- forms may only submit back to your origin.frame-ancestors 'none'- the page may not be framed by anyone (clickjacking off).object-src 'none'- no plugin content.upgrade-insecure-requests- rewrite anyhttp://subresource tohttps://.report-to csp-endpoint- send violation reports to the named reporting group.
The nonce value must be unique per response and unpredictable. Generate it server-side for each request and stamp it onto both the header and every inline <script nonce="..."> you control.
Common weak patterns
These are the patterns that make a policy look present while doing little. Each one widens the allowlist back toward "anything goes."
'unsafe-inline'inscript-src- re-permits exactly what XSS produces: arbitrary inline scripts and event handlers. With it set, an injected<script>runs normally and the policy buys you almost nothing on the script axis.'unsafe-eval'- allowseval,new Function, and string timers, turning attacker-controlled strings into executable code. Some legacy template engines need it; treat that as debt to remove, not a default.- Wildcard
*sources -script-src *orconnect-src *permits any host, so exfiltration and remote-script loading work freely. A wildcard is the absence of a policy with extra steps. data:inscript-src- lets an attacker run code straight from adata:URI (data:text/javascript,...) without ever touching an allowlisted host. Never allowdata:for scripts.- Missing
frame-ancestors- leaves the page framable and open to clickjacking. Relying only onX-Frame-Optionsmeans no multi-origin allowlisting and inconsistent handling across edge cases the newer directive covers. - Allowlisting a whole CDN - if that CDN hosts any file that can be coerced into executing attacker input (a JSONP endpoint, an old vulnerable library, an Angular build), the attacker borrows your trust in the CDN to run code. Pin specific paths or use hashes instead of trusting an entire host.
https:as a source -script-src https:trusts every HTTPS host on the internet. It blocks plainhttpand nothing else, which is essentially no restriction for an attacker who controls or finds one usable HTTPS endpoint.
Rollout: report-only to enforced
Shipping a strict CSP straight to enforcement on a live app almost always breaks something - a third-party widget, an inline handler you forgot, a stylesheet on an unexpected host. The safe path is to observe first.
- Deploy in report-only mode. Send your candidate policy in the
Content-Security-Policy-Report-Onlyheader alongside a reporting endpoint. In this mode the browser does not block anything; it only reports what would have been blocked. Your site keeps working while you collect data. - Collect from real traffic for about a week. Synthetic testing misses the long tail of browsers, extensions, and user paths. Roughly a week of live traffic surfaces the violations that actually occur.
- Triage the violations. Separate legitimate breakage (your own widget on a host you forgot to allow) from noise and attacks. Browser extensions and injected adware generate a lot of report spam that is not your code - learn to filter it out so it does not distort the policy.
- Tighten, do not loosen. Add the specific hosts, nonces, or hashes that legitimate features need. Resist the urge to paper over a cluster of violations with
'unsafe-inline'or a wildcard, which throws away the whole exercise. - Switch to enforcement. Once report-only is quiet except for known noise, move the same policy into the enforcing
Content-Security-Policyheader. Keep a reporting endpoint live afterward so regressions and real attacks still surface.
Nonces and hashes
Removing 'unsafe-inline' means you need another way to permit the inline scripts you genuinely want. There are two mechanisms.
A nonce is a random per-response token you put in both the header (script-src 'nonce-abc123') and the tag (<script nonce="abc123">). The browser runs only inline scripts whose nonce matches. Because the value changes every response and an attacker cannot guess it, injected scripts have no valid nonce and are blocked. Nonces suit server-rendered pages where you can generate a fresh value per request.
A hash permits one exact inline script by its content digest. You compute the SHA-256 of the script's body, base64-encode it, and add it to the policy as 'sha256-...'. The browser hashes each inline script and runs only those whose hash is listed. Hashes suit static inline snippets that do not change, so you can compute the digest once at build time. If even one byte of the script changes, the hash no longer matches and you must regenerate it.
script-src 'self' 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=';
You can generate the base64 SHA-256 digest of an inline snippet with this site's Hash Generator - hash the exact script contents, prefix the result with sha256-, and drop it into the directive.
Related tools
- CSP Analyzer - paste a policy to spot weak directives and missing protections
- Hash Generator - compute the SHA-256 digest for a
'sha256-...'script-src entry - Secure Paste - share a policy or report snippet without leaving it on a third-party host
Related guides
- Web Security Guide - the broader picture CSP fits into
- JWT Decoding Safely - handling tokens that CSP's
connect-srchelps protect in transit - Local-First Dev Tools - why running tooling locally keeps sensitive data off other origins