API middleware: - SecurityHeaders now sets Content-Security-Policy from config/security.php - Default API policy: "default-src 'none'; frame-ancestors 'none'" - Supports report-only mode via CSP_REPORT_ONLY env var - Policy value configurable via CSP_POLICY env var Nginx deployment configs (deploy/nginx/): - security-headers.conf: shared headers for all server blocks - csp-api.conf: restrictive JSON-only policy for api.crewli.app - csp-spa.conf: SPA policy for app/admin (self + unsafe-inline styles) - csp-portal.conf: portal policy matching SPA Development: - CSP meta tags added to all three index.html files - Includes 'unsafe-inline' + 'unsafe-eval' for Vite HMR/loader script - Each app allows its own ws:// port for HMR websocket Resolves security finding A13-9. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
39 lines
1.2 KiB
PHP
39 lines
1.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
final class SecurityHeaders
|
|
{
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
$response = $next($request);
|
|
|
|
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
|
$response->headers->set('X-Frame-Options', 'DENY');
|
|
$response->headers->set('X-XSS-Protection', '0');
|
|
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
$response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
|
|
if ($request->isSecure() || app()->environment('production')) {
|
|
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
}
|
|
|
|
$csp = config('security.csp');
|
|
if ($csp) {
|
|
$headerName = config('security.csp_report_only')
|
|
? 'Content-Security-Policy-Report-Only'
|
|
: 'Content-Security-Policy';
|
|
|
|
$response->headers->set($headerName, $csp);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
}
|