Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Content Security Policy CSP for Laravel Apps

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.

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.

CSP Enforcement in the BrowserLaravelHTTP responseCSP Headerscript-src connect-srcBrowserPolicy engineAllowed resourceVite bundle + nonceBlocked resourceInjected script tagReport URIViolation logDefault deny for scripts unless whitelisted or nonce-validWorks alongside Laravel escaping and CSRF tokens
How Content Security Policy CSP for Laravel Apps blocks unauthorised scripts while allowing Vite assets with valid nonces

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.

Laravel CSP Middleware PipelineRequestGenerate nonceControllerBlade viewVite @vite tagnonce attributePolicy builderdirectives arraySet CSP headeron HTML responseNonce is generated once per request and shared with viewsApply middleware globally or to web route group only
Laravel middleware generates a per-request CSP nonce, renders Blade with Vite, then attaches the Content-Security-Policy header

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.

DirectiveWhat it controlsLaravel-specific notesStrict starting point
default-srcFallback for unspecified fetch typesSets baseline; tighten other directives explicitly'self'
script-srcJavaScript executionVite bundles, Stripe.js, analytics, Livewire/Alpine inline bootstraps'self' 'nonce-…' plus known CDNs
style-srcCSS loading and inline stylesTailwind and Bootstrap often need 'unsafe-inline' unless you hash every style block'self' 'unsafe-inline' initially
connect-srcfetch, XHR, WebSocket endpointsSanctum APIs, Stripe API, Pusher, Meilisearch, Khalti/eSewa callbacks'self' plus payment and search hosts
img-srcImages and faviconsSpatie Media Library on S3, Gravatar, user uploads'self' data: https: then narrow
frame-srcEmbedded iframesStripe Elements, YouTube embeds, Google reCAPTCHAExplicit payment and captcha domains
form-actionForm submission targetsPrevents 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.

CSP Rollout Stages for LaravelStage 1Report-OnlyLog violationsStage 2Enforce scriptsNonce + CDNsStage 3Tight connect-srcRemove wildcardsProduction checklistStripe frame-src + script-src whitelistedS3 img-src bucket host added explicitlyNo unsafe-eval unless legacy lib requires it
Staged Content Security Policy CSP rollout for Laravel Apps: report-only logging first, then enforced script-src, then tightened connect-src

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.

  1. Deploy report-only CSP to staging and production simultaneously.
  2. Exercise every checkout, upload, search, and admin workflow.
  3. Collect violation reports for at least one week of real traffic.
  4. Fix missing hosts and add nonces to inline blocks.
  5. Switch to enforcing Content-Security-Policy on public routes.
  6. 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.

CSP Gotchas in Laravel Productionunsafe-inline on script-srcNegates XSS protectionCached HTML + old nonceScripts fail silentlyMissing connect-srcAPI and WS calls blockedThird-party iframesframe-src not updatedFix: report-only first, nonce Vite, split admin policyTest payments and uploads on every release
Frequent Content Security Policy CSP for Laravel Apps mistakes and the production fixes that prevent checkout and admin regressions

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 @vite and 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, and frame-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

A browser-enforced whitelist sent as an HTTP header. Only scripts, styles, fonts, images, and API endpoints you explicitly allow can run or load on your Laravel pages.

No. Laravel 12 and 13 do not ship an enforcing CSP. You add it via custom middleware, spatie/laravel-csp, or web-server configuration.

Yes. Ship Content-Security-Policy-Report-Only for one or two release cycles, collect violations, then switch to enforcing mode on public routes.

Create middleware with php artisan make:middleware ContentSecurityPolicy, generate a per-request nonce, build directive strings, and attach the header to HTML responses only. Register it in bootstrap/app.php using the Laravel 11+ middleware API. Share the nonce via view()->share so Blade and Vite can use it. Skip CSP entirely on API-only JSON routes because browsers enforce CSP on documents, not application/json responses. Pair the header with X-Frame-Options or frame-ancestors, X-Content-Type-Options nosniff, and strict HTTPS for layered protection.

CSP reduces XSS impact by stopping unauthorised script execution in the browser, even when HTML sanitisation fails. A missed unescaped Blade output, stored user comment, or compromised CDN asset may still reach the page, but the browser refuses to run code you never whitelisted. It is defence in depth, not a substitute for escaping with double braces, prepared statements, CSRF protection, or authorisation checks. On legal-tech portals I have maintained, CSP reduced the blast radius of third-party widgets and admin rich-text fields.

Strict script-src blocks inline scripts unless they carry a matching nonce. Laravel middleware generates a fresh base64-encoded nonce per request, stores it on the request and shares it as cspNonce. Pass it to @vite with the nonce attribute in your layout, and add nonce="{{ $cspNonce }}" to any unavoidable inline script blocks. Vite 8.x bundles work with this pattern. Never reuse nonces across requests, and avoid caching personalised HTML that embeds them, or scripts fail silently on the next load.

For typical Laravel monoliths, five directives carry most value. script-src controls Vite bundles, Stripe.js, analytics, and Livewire bootstraps — start with self plus nonce and known CDNs. style-src often needs self and unsafe-inline initially because Tailwind and Bootstrap inject inline styles. connect-src must list Sanctum APIs, Stripe, Pusher, Meilisearch, and payment callback hosts. img-src covers Spatie Media Library, Gravatar, and uploads — self, data, and https is a practical start. form-action set to self prevents form exfiltration to attacker domains.

Whitelist third-party integrations explicitly rather than loosening the whole policy. Stripe Checkout and Elements need script-src and frame-src entries for https://js.stripe.com plus connect-src for https://api.stripe.com. Livewire 3 expects script-src self and posts back to the same origin on connect-src. Load Google Tag Manager from its canonical host and allow only the tag domains it actually calls, instead of keeping unsafe-inline on script-src permanently. Deploy report-only CSP to staging and production simultaneously, exercise every checkout and admin workflow for at least one week, then enforce on public routes and regression-test payments in staging with CSP fully active.

Both work on Laravel 12 and 13 with PHP 8.3 or higher. Custom middleware gives full control over nonce generation, environment-specific directives, and splitting policies between public and admin route groups. spatie/laravel-csp is a maintained community package that structures policy classes and reduces boilerplate. Application middleware is usually easier to version-control alongside Vite and payment integrations. Choose based on whether you want a package abstraction or a lightweight class you own entirely. Either approach beats web-server-only configuration when policies differ between local Vite dev and production builds.

Browsers enforce Content Security Policy on HTML documents, not on application/json responses. API clients and mobile apps ignore CSP headers entirely. Your API surface still needs authentication, rate limiting, and input validation independent of CSP. Applying CSP middleware globally adds unnecessary header processing and can confuse debugging. The article recommends applying CSP only when the response Content-Type includes text/html, using a shouldApplyCsp check in middleware. Focus CSP effort on Blade-rendered pages where XSS payloads could execute in a user browser.

Teams often add unsafe-inline to script-src as a permanent fix, which disables most XSS benefit — use nonces or external files instead. Forgetting CDN hosts after referencing assets off-origin breaks scripts even when script-src self is set. Caching marketing HTML at the edge with embedded nonces causes silent failures on the next request. Tightening script-src without updating connect-src breaks AJAX, Sanctum calls, and WebSocket connections — browser consoles show connect violations clearly. Enforcing without ongoing monitoring means new marketing tags break silently; keep report-uri or Reporting API endpoints active and alert on violation spikes after enforcement.

Nepal payment gateways often redirect or embed checkout iframes, so you need more than script-src self. Add their checkout domains to frame-src for embedded widgets and connect-src for callback and API requests during payment verification. Khalti, eSewa, and IME Pay flows should be regression-tested in staging with Content-Security-Policy enforced, not report-only, before you promote the release. Payment integrations on Laravel apps break easily when connect-src is tightened before script-src and frame-src are complete. Treat each gateway domain as an explicit allowlist entry and retest the full checkout path after every policy change.

Yes, when back-office tools need broader rules. WYSIWYG editors inject inline styles and scripts that conflict with strict public policies. On client portals I have built, admin-only routes can use relaxed middleware while public pages stay tight. Register separate middleware groups in bootstrap/app.php — web public routes get the strict policy, admin routes get a controlled exception stack. Document-heavy sites such as Notary Nepal or Court Marriage In Nepal benefit from the strictest policy on public pages, while authenticated staff areas may need exceptions for file previews and embedded maps. Split policies limit XSS exposure where it matters most.

For style-src, self plus unsafe-inline is a practical starting point because Tailwind and Bootstrap often inject inline styles, and hashing every style block is heavy upfront. For script-src, unsafe-inline is only acceptable as a temporary bridge during rollout — it largely disables XSS protection and should be replaced with per-request nonces or external files as fast as possible. Avoid unsafe-eval unless a legacy library demands it, because it weakens protection significantly. Analytics teams sometimes cite GTM as a reason to keep unsafe-inline on script-src, but loading GTM from its canonical host and whitelisting only the domains it calls is the better long-term fix.

CSP limits what runs in the browser after HTML reaches the client. It does not replace output escaping in Blade, prepared statements against SQL injection, CSRF tokens, session hardening, or role-based access control with packages like Spatie Laravel Permission. Server-side validation and authorisation still matter because attackers can exploit logic bugs CSP never sees. Pair CSP headers with HTTPS, server hardening on Ubuntu, correct file permissions, and up-to-date PHP. Correct Content-Security-Policy headers on an app running outdated PHP or exposed admin ports still leave gaps — application and infrastructure security belong in the same release checklist.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: