81 lines
3.0 KiB
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;
|
|
}
|
|
}
|