Cross-Site Scripting (XSS) remains one of the most prevalent and damaging vulnerabilities in modern web applications. Even with robust template escaping and input validation frameworks, a single unescaped variable in a rich text editor or third-party npm package can allow attackers to steal session tokens, exfiltrate private user data, or inject malicious cryptocurrency miners.
The web browser's ultimate defensive layer against XSS is Content Security Policy (CSP). However, legacy "allowlist-based" CSP policies (e.g., script-src 'self' https://cdn.example.com) are notoriously fragile, hard to maintain, and trivially bypassable via open redirects or JSONP endpoints.
The modern industry standard (CSP Level 3) abandons domain allowlists in favor of Cryptographic Nonces and the 'strict-dynamic' directive. In this guide, we dive into how nonce-based CSP works, build a production-grade Laravel CSP middleware with Blade directives, enforce Subresource Integrity (SRI), and safely deploy policies using Report-Only mode.
The Flaw in Legacy Allowlist CSP vs. Nonce-Based CSP
| Security Model | Example Policy | Vulnerabilities & Maintenance Cost |
|---|---|---|
| Legacy Domain Allowlist (CSP Level 1/2) | script-src 'self' https://apis.google.com https://cdn.jsdelivr.net; |
High bypass risk: If any allowlisted CDN hosts an Angular or JSONP library, attackers can bypass CSP using gadget chains. Fragile to maintain. |
| Modern Nonce-Based (CSP Level 3) | script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline'; |
Zero bypass risk: Only scripts tagged with the unguessable per-request cryptographically random nonce can execute. The browser ignores 'unsafe-inline' when nonces are present. |
How 'strict-dynamic' Works
When you specify 'strict-dynamic' in a CSP Level 3 policy:
- The browser executes only inline and external
<script>tags that contain a matchingnonce="RANDOM_VALUE"attribute. - Any script that has been legitimately trusted via a nonce is permitted to dynamically create and load new child scripts (such as Google Tag Manager, Stripe Elements, or Chart.js) without requiring you to manually allowlist their host domains.
- Injected malicious HTML (such as
<img src=x onerror=alert(1)>or<script>malicious()</script>) is completely blocked because the attacker cannot guess the unique per-request nonce.
1. Implementing Nonce-Based CSP in Laravel
Step 1: The CSP Middleware
Create app/Http/Middleware/ContentSecurityPolicy.php:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class ContentSecurityPolicy
{
public function handle(Request $request, Closure $next): Response
{
// Generate a 128-bit cryptographically secure random nonce for this request
$nonce = base64_encode(random_bytes(16));
$request->attributes->set('csp_nonce', $nonce);
// Share the nonce directly with Vite tag generators
Vite::useCspNonce($nonce);
/** @var Response $response */
$response = $next($request);
// Construct modern CSP Level 3 policy
$csp = [
"default-src 'self'",
"script-src 'nonce-{$nonce}' 'strict-dynamic' 'unsafe-inline' https: http:",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com data:",
"img-src 'self' data: https://cdn.sejan.dev blob:",
"connect-src 'self' https://cdn.sejan.dev https://www.google-analytics.com",
"frame-ancestors 'none'", // Clickjacking protection (supersedes X-Frame-Options)
"base-uri 'self'", // Prevents HTML base tag hijacking
"form-action 'self'", // Prevents form submission redirection
"object-src 'none'", // Disables Flash / Java Applet exploits
];
$response->headers->set('Content-Security-Policy', implode('; ', $csp));
return $response;
}
}
Step 2: Registering a Custom Blade Directive (@nonce)
In app/Providers/AppServiceProvider.php, register a helper directive for custom scripts:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Blade::directive('nonce', function () {
return '<?php echo "nonce=\"" . request()->get("csp_nonce", request()->attributes->get("csp_nonce")) . "\""; ?>';
});
}
}
Step 3: Using the Nonce in Blade Templates
<!-- Vite automatically injects the nonce into compiled script bundles -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
<!-- Inline scripts use the custom @nonce directive -->
<script @nonce>
console.log('This secure inline script executes seamlessly because of the valid nonce!');
</script>
2. Subresource Integrity (SRI) for Third-Party CDNs
If your application loads static assets from external CDNs, use Subresource Integrity (SRI) to ensure that a compromised CDN cannot serve tampered JavaScript:
<!-- Subresource Integrity validates cryptographic hash before execution -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"
integrity="sha512-D9gUyxqja7hBtkWDLmUcjSXAtUwIx524UWDgkQ0PfeIsZ0o6IEvOffvPU8Zch5PCXYuu4EC55610LBkcGT5Juw=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
3. Safe Rollout: Content-Security-Policy-Report-Only
Never deploy an untested CSP policy directly in blocking mode on high-traffic production apps. Instead, utilize Report-Only mode:
// Header: Content-Security-Policy-Report-Only
$response->headers->set(
'Content-Security-Policy-Report-Only',
implode('; ', $csp) . '; report-uri /api/csp-violation-report'
);
In Report-Only mode, the browser logs violations to your reporting endpoint without blocking user execution, allowing you to identify missing nonces or third-party asset domains safely.
Production CSP Security Checklist
- ✓ Always use fresh per-request nonces: Never cache nonces across HTTP requests or between different sessions.
- ✓ Set
frame-ancestors 'none': Defends against Clickjacking and UI redressing attacks across all modern browsers. - ✓ Set
object-src 'none': Neutralizes malicious PDF, Flash, or ActiveX plugins completely. - ✓ Set
base-uri 'self': Prevents attackers from injecting<base href="...">tags to hijack relative URLs.
Conclusion
Modern Content Security Policy Level 3 transforms web security from a brittle domain guessing game into an impenetrable cryptographic boundary. By combining per-request nonces with 'strict-dynamic' in Laravel, you achieve total defense against Cross-Site Scripting while preserving seamless developer velocity.