
September 09, 2026
13 min read
By Kokil Thapa | Last reviewed: September 2026
Cross-site scripting still ranks among the most common ways attackers compromise production Laravel applications. Content Security Policy CSP for Laravel Apps gives you a browser-enforced whitelist: only scripts, styles, fonts, images, and API endpoints you explicitly allow can run or load. Laravel does not ship a strict CSP by default, but the framework makes it straightforward to add headers through middleware, nonces for Vite bundles, and a staged report-only rollout. This guide walks through a production-ready setup you can deploy on Laravel 12 or 13 without breaking payments, analytics, or Livewire.
Content-Security-Policy HTTP header from middleware, whitelisting trusted sources in directives like script-src and connect-src, and using per-request nonces for inline scripts generated by Vite or Blade.What is Content Security Policy and why does Laravel need it?
CSP is a W3C standard delivered as an HTTP response header. The browser reads it before executing page resources. If a script tag points to an attacker-controlled domain, the browser blocks it. If an injected inline script lacks a valid nonce, the browser blocks that too.
Laravel apps are XSS targets for a simple reason. Blade templates often mix server-rendered HTML with user input, third-party widgets, and JavaScript from Vite. Escaping output with {{ }} helps, but it is not a complete defence. A missed {!! !!}, a stored comment field, or a compromised CDN asset bypasses escaping alone.
CSP adds defence in depth. Even when HTML sanitisation fails, the browser still refuses to run code you never authorised. On legal-tech portals and client document portals I have maintained, CSP reduced the blast radius of third-party script tags and admin-panel rich-text fields.
The policy sits alongside other headers you should already send. Pair CSP with X-Frame-Options or frame-ancestors, X-Content-Type-Options: nosniff, and strict HTTPS. A hardened Ubuntu stack plus application-level controls gives you layered protection, similar to what I describe in broader server security hardening work.
What CSP does not replace
CSP is not a substitute for output escaping, prepared statements, or authorisation checks. It limits what runs in the browser after HTML reaches the client. Server-side validation and RBAC still matter.
How do you add CSP headers in a Laravel application?
The cleanest approach is middleware that builds the policy on every HTML response. You can write custom middleware or use the community spatie/laravel-csp package. Both work on Laravel 12 and 13 with PHP 8.3 or higher.
Step 1: Create CSP middleware
Generate middleware with Artisan, then attach a policy string or builder class:
php artisan make:middleware ContentSecurityPolicy
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class ContentSecurityPolicy
{
public function handle(Request $request, Closure $next): Response
{
$nonce = base64_encode(Str::random(32));
$request->attributes->set('csp_nonce', $nonce);
view()->share('cspNonce', $nonce);
$response = $next($request);
if ($this->shouldApplyCsp($response)) {
$policy = implode('; ', [
"default-src 'self'",
"base-uri 'self'",
"object-src 'none'",
"frame-ancestors 'self'",
"script-src 'self' 'nonce-{$nonce}' https://js.stripe.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' https://api.stripe.com",
"form-action 'self'",
"upgrade-insecure-requests",
]);
$response->headers->set('Content-Security-Policy', $policy);
}
return $response;
}
private function shouldApplyCsp(Response $response): bool
{
return str_contains($response->headers->get('Content-Type', ''), 'text/html');
}
}
Register the middleware in bootstrap/app.php on Laravel 11+ style apps:
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\ContentSecurityPolicy::class);
})
For API-only JSON routes, skip CSP entirely. Browsers enforce CSP on documents, not on application/json responses. Your API surface still needs auth, rate limits, and input validation.
Step 2: Start in report-only mode
Ship Content-Security-Policy-Report-Only first. The browser logs violations but does not block resources. Collect reports for one or two release cycles before switching to enforcing mode.
$response->headers->set(
'Content-Security-Policy-Report-Only',
$policy . "; report-uri /csp-violation-report"
);
Add a lightweight POST route that stores violations. Use your existing logging stack or forward to a collector. During report-only, pair the work with dynamic security testing on staging.
Step 3: Wire nonces into Vite and Blade
Laravel Vite 8.x supports a CSP nonce through the @vite directive when you pass the nonce attribute. In your layout:
<!-- resources/views/layouts/app.blade.php -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
@vite(['resources/css/app.css', 'resources/js/app.js'], nonce: $cspNonce)
</head>
<body>
@yield('content')
</body>
</html>
For any unavoidable inline script, add the same nonce:
<script nonce="{{ $cspNonce }}">
window.appConfig = @json(['locale' => app()->getLocale()]);
</script>
Never copy a nonce across requests. Each page load needs a fresh value. Cached full-page HTML with a stale nonce breaks scripts silently.
Which CSP directives matter most for Laravel Blade and Vite apps?
A strict policy has many directives. For typical Laravel monoliths, these five carry most of the security value.
| Directive | What it controls | Laravel-specific notes | Strict starting point |
|---|---|---|---|
default-src | Fallback for unspecified fetch types | Sets baseline; tighten other directives explicitly | 'self' |
script-src | JavaScript execution | Vite bundles, Stripe.js, analytics, Livewire/Alpine inline bootstraps | 'self' 'nonce-…' plus known CDNs |
style-src | CSS loading and inline styles | Tailwind and Bootstrap often need 'unsafe-inline' unless you hash every style block | 'self' 'unsafe-inline' initially |
connect-src | fetch, XHR, WebSocket endpoints | Sanctum APIs, Stripe API, Pusher, Meilisearch, Khalti/eSewa callbacks | 'self' plus payment and search hosts |
img-src | Images and favicons | Spatie Media Library on S3, Gravatar, user uploads | 'self' data: https: then narrow |
frame-src | Embedded iframes | Stripe Elements, YouTube embeds, Google reCAPTCHA | Explicit payment and captcha domains |
form-action | Form submission targets | Prevents exfiltration via crafted forms | 'self' |
Official reference material lives in the MDN Content Security Policy documentation. Cross-check directive syntax there before you paste policies into production.
For Vue with Laravel, connect-src must include your API origin and any WebSocket host Reverb or Pusher uses. For Livewire, keep 'self' on script and connect directives because Livewire posts back to the same app origin.
Environment-specific policies
Keep separate policy classes for local, staging, and production. Local dev often loads Vite from http://127.0.0.1:5173. Production serves hashed files from /build. Hard-coding one policy breaks the other environment.
// config/csp.php (custom config file)
return [
'enabled' => env('CSP_ENABLED', true),
'report_only' => env('CSP_REPORT_ONLY', true),
'directives' => [
'script-src' => array_filter([
"'self'",
'nonce' => true,
app()->environment('local') ? 'http://127.0.0.1:5173' : null,
'https://js.stripe.com',
]),
],
];
Validate policy strings with a regex tester or a JSON formatter when you store violation payloads. Small syntax errors make the entire header ignored by some browsers.
How do you roll out CSP without breaking Stripe, analytics, or Livewire?
Most production breakages come from third-party scripts, not your own Vite output. Treat integrations as an explicit allowlist exercise.
Payment gateways and embedded widgets
Stripe Checkout and Elements need both script-src and frame-src entries for https://js.stripe.com. Khalti, eSewa, and IME Pay often redirect or embed checkout iframes. Add their checkout domains to frame-src and connect-src. Payment flows on Laravel payment integrations should be regression-tested in staging with CSP enforced, not report-only.
Google Tag Manager and analytics
Analytics is the usual reason teams keep unsafe-inline on script-src. Better options exist. Load GTM from its canonical host, then allow only the tag domains GTM actually calls. Some teams proxy analytics through their own subdomain. That reduces third-party hosts but adds operational work.
Livewire, Alpine, and inline bootstraps
Livewire 3 ships JavaScript that expects to run from your app origin. Keep script-src 'self' and pass nonces into the layout Livewire uses. Alpine stores attached with x-data do not need separate hosts. Avoid unsafe-eval unless a legacy library demands it. unsafe-eval weakens XSS protection significantly.
Admin panels and rich text editors
WYSIWYG editors sometimes inject inline styles and scripts. On a client portal like Mijar Law Associates, admin-only routes can use a relaxed policy while public pages stay strict. Split policies by middleware group: web public routes get the tight policy, admin routes get a separate middleware stack.
- Deploy report-only CSP to staging and production simultaneously.
- Exercise every checkout, upload, search, and admin workflow.
- Collect violation reports for at least one week of real traffic.
- Fix missing hosts and add nonces to inline blocks.
- Switch to enforcing
Content-Security-Policyon public routes. - Re-run automated scans and manual smoke tests after deploy.
Automate the last step in your pipeline. A GitLab CI deploy can curl staging and assert the CSP header is present before promoting the release.
What are common CSP mistakes on production Laravel deployments?
These failures show up repeatedly on apps I audit or maintain.
Using unsafe-inline as a permanent fix
Adding 'unsafe-inline' to script-src disables most XSS benefit. Use it only as a temporary bridge. Move inline blocks behind nonces or external files as fast as possible.
Forgetting build assets after deploy
Vite emits hashed filenames under public/build. CSP with script-src 'self' still works because paths stay same-origin. Problems appear when you reference a CDN for assets but forget to add that host. Commit built assets if your server lacks Node, then verify headers after symlink deploys.
Caching HTML with stale nonces
Full-page Redis caching of authenticated HTML is rare, but marketing pages sometimes get cached at the edge. A cached nonce fails on the next request. Either exclude personalised HTML from cache or use hash-based CSP for static inline snippets.
Missing connect-src for AJAX and WebSockets
Developers tighten script-src then wonder why fetch calls fail. Browser consoles show CSP connect violations clearly. Add every API subdomain, search cluster, and websocket host explicitly.
No monitoring after enforcement
Once enforcing, new marketing tags or A/B scripts will break silently for users. Keep report-uri or the Reporting API endpoint active. Forward reports to your logging stack and alert on spikes.
Pair header work with server hardening. Correct Content-Security-Policy headers on an app running outdated PHP or exposed admin ports still leave gaps. Linux administration and application security belong in the same release checklist.
Testing tools worth using
Browser DevTools lists CSP violations in the console. The OWASP CSP Cheat Sheet gives directive examples aligned with common attack patterns. Laravel's own documentation on middleware and responses explains where headers attach in the request lifecycle — see the Laravel 12 middleware docs for registration patterns on modern app skeletons.
On document-heavy sites such as Notary Nepal or Court Marriage In Nepal, public pages benefit from the strictest policy. Authenticated staff areas may need controlled exceptions for file previews and embedded maps.
Key Takeaways
- Implement Content Security Policy CSP for Laravel Apps through middleware that sets headers on HTML responses, not on JSON API routes.
- Generate a fresh nonce per request, pass it to
@viteand any inline scripts, and avoid caching personalised HTML that embeds nonces. - Start with
Content-Security-Policy-Report-Only, collect violations for a full traffic cycle, then enforce on public routes first. - Whitelist payment, analytics, and iframe hosts explicitly in
script-src,connect-src, andframe-src— test checkout after every policy change. - Keep admin and public policies separate when WYSIWYG editors or embedded widgets require broader rules on back-office pages only.
- Combine CSP with escaping, CSRF protection, HTTPS, and server hardening — headers alone do not fix server-side bugs.
People Also Ask
Does Laravel include Content Security Policy by default?
No. Laravel 12 and 13 do not ship an enforcing CSP out of the box. You add it via custom middleware, a package such as spatie/laravel-csp, or web-server configuration. Application middleware is usually easier to version-control alongside Vite and payment integrations.
Can CSP block XSS in Laravel Blade templates?
CSP reduces XSS impact by stopping unauthorised script execution in the browser. It does not remove the need to escape user output in Blade. Use {{ $variable }} for untrusted data, CSP for defence in depth, and sanitise HTML when rich text is a business requirement.
Should I use CSP nonces or hashes for Vite assets?
Nonces fit Laravel's per-request middleware model and work well with Vite's @vite nonce support. Hashes suit static inline snippets that never change. Most teams nonce dynamic layouts and hash only fixed error-page scripts.
What is the difference between Content-Security-Policy and Report-Only?
Enforcing headers block violations. Report-Only headers log them without blocking. Always validate a new policy in Report-Only on staging and production before you cut over, especially when marketing tags or payment scripts are involved.
Ship CSP on your next Laravel release
Content Security Policy CSP for Laravel Apps is one of the highest-return security controls you can add without rewriting business logic. Middleware, a per-request nonce, report-only staging, and explicit third-party allowlists get you most of the way there. On the next sprint, add the middleware, turn on report-only in production, and fix whatever violations appear before you enforce.
If you want help auditing headers, payment flows, or a full hardening pass on a live app, see testing and optimisation services or ongoing support. For a greenfield build with security baked in from day one, contact us and outline your stack, integrations, and deployment target.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

