clusev/app/Http/Middleware/SecurityHeaders.php

81 lines
3.0 KiB
PHP

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Adds hardening response headers to every web response.
*
* The Content-Security-Policy is deliberately PRAGMATIC: Livewire and Alpine
* inject inline scripts/handlers and Alpine relies on `eval`, so 'unsafe-eval'
* and 'unsafe-inline' are permitted for scripts/styles. connect-src allows
* ws:/wss: so Reverb (realtime) and Vite HMR keep working in dev. This CSP can
* be TIGHTENED LATER (e.g. nonce-based scripts, dropping 'unsafe-*') once the
* app no longer depends on inline/eval — start strict on the structural
* directives (base-uri, frame-ancestors) and relax only what the stack needs.
*/
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$headers = $response->headers;
$headers->set('X-Frame-Options', 'DENY');
$headers->set('X-Content-Type-Options', 'nosniff');
$headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$headers->set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=(), interest-cohort=()'
);
// Livewire/Alpine need inline + eval; Reverb rides ws/wss.
$scriptSrc = ["'self'", "'unsafe-eval'", "'unsafe-inline'"];
$styleSrc = ["'self'", "'unsafe-inline'"];
$connectSrc = ["'self'", 'ws:', 'wss:'];
$fontSrc = ["'self'"];
// In dev, Vite serves the bundle + HMR from a SEPARATE origin (e.g.
// http://host:5173). A same-origin CSP would block it and break the app,
// so allow the running dev-server origin (from public/hot) outside prod.
// In production assets are same-origin (/build) and this branch is inert.
if (! app()->isProduction() && is_file(public_path('hot'))) {
$dev = trim((string) file_get_contents(public_path('hot')));
if ($dev !== '') {
$devWs = preg_replace('#^http#', 'ws', $dev);
array_push($scriptSrc, $dev);
array_push($styleSrc, $dev);
array_push($fontSrc, $dev);
array_push($connectSrc, $dev, $devWs);
}
}
$headers->set('Content-Security-Policy', implode('; ', [
"default-src 'self'",
'script-src '.implode(' ', $scriptSrc),
'style-src '.implode(' ', $styleSrc),
'connect-src '.implode(' ', $connectSrc),
"img-src 'self' data:",
'font-src '.implode(' ', $fontSrc),
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
]));
// Only assert HSTS over an already-secure connection so we never
// pin clients to HTTPS on a plain-HTTP dev/LAN deployment.
if ($request->isSecure()) {
$headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}
return $response;
}
}