Skip to content
Rapcsány Krisztián Rapcsány Krisztián
hu
← All articles
2 min read

A strict CSP in Laravel, with Vite

Laravel Security Vite

A Content Security Policy (CSP) tells the browser where scripts on the page may come from. If someone manages to inject a foreign script, for example through an input field, the browser does not run it. The hard part is that my own inline scripts have to be allowed too, without turning them into a loophole.

Nonce: one identifier per request

The middleware generates a random identifier on every request. I put it in the header and on every one of my inline scripts. The browser only runs a script whose identifier matches. Vite can apply it to its own tags as well.

SecurityHeaders.php
$nonce = base64_encode(random_bytes(16));

Vite::useCspNonce($nonce);         // Vite puts the nonce on its own tags
View::share('cspNonce', $nonce);   // used as <script nonce="..."> on every inline script

$response = $next($request);

$response->headers->set('Content-Security-Policy', implode('; ', [
    "default-src 'self'",
    "script-src 'self' 'nonce-{$nonce}'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data:",
    "font-src 'self'",
    "object-src 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "frame-ancestors 'self'",
]));

An excerpt of real code, anonymised.

What to watch out for

  • Because of the Vite dev server I turn the CSP off while it runs, otherwise development does not work.
  • For styles I kept inline allowed, because style attributes cannot be protected with an identifier.
  • I only allow an external service (analytics, captcha) when it is configured.
  • A test checks that every inline script has an identifier, so a new script cannot slip through unprotected.

I check the result in the browser console: if something is blocked, it shows up there immediately.

Working on a similar problem?

If you are stuck, or want a second look at your solution, write to me. I am happy to look.

Write to me →
Get a quote →