Hardening HTTP, CSP & Control de Caché
La seguridad perimetral de Mañuti se consolida en el transporte HTTP, neutralizando vectores de inyección, manipulación de tipos MIME, exposición de datos en memoria intermedia del navegador e incrustación maliciosa en marcos externos.
1. El Middleware SecurityHeaders
Se ejecuta como middleware global dentro del kernel (bootstrap/app.php), alterando las cabeceras de respuesta antes de ser despachadas por Nginx:
// app/Http/Middleware/SecurityHeaders.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// 1. Anti-Clickjacking: Prohíbe framing desde orígenes foráneos
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
// 2. Anti-MIME Sniffing: Fuerza el apego al Content-Type declarado
$response->headers->set('X-Content-Type-Options', 'nosniff');
// 3. Referrer Policy: Preserva privacidad en referencias salientes
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
// 4. HSTS: Conexión forzada bajo TLS (Activado en Prod/HTTPS)
if ($request->isSecure() || app()->environment('production')) {
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
}
// 5. Content Security Policy (CSP) Estricto
$csp = "default-src 'self'; " .
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://unpkg.com; " .
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://unpkg.com; " .
"font-src 'self' https://fonts.gstatic.com data:; " .
"img-src 'self' data: https: " . env('AWS_ENDPOINT', '') . "; " .
"object-src 'self' " . env('AWS_ENDPOINT', '') . "; " .
"connect-src 'self' ws: wss: http: https:;";
$response->headers->set('Content-Security-Policy', preg_replace('/\s+/', ' ', trim($csp)));
// 6. Permissions Policy: Deshabilita hardware no empleado
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
return $response;
}
}
2. Inmunización del Historial del Navegador (PreventBackHistory)
Para cumplir con ISO 27001 sobre descarte seguro de sesiones en equipos compartidos, el middleware con alias 'no-cache' impide que el botón "Atrás" del navegador (Back-Forward Cache o bfcache) renderice vistas confidenciales tras cerrar sesión:
// app/Http/Middleware/PreventBackHistory.php
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate');
$response->headers->set('Pragma', 'no-cache');
$response->headers->set('Expires', 'Sun, 02 Jan 1990 00:00:00 GMT');
return $response;
}
Flujo de Neutralización
- El operador cierra sesión en
/logout. La sesión en Redis es invalidada. - Un atacante local pulsa el botón "Atrás" en el navegador.
- Dado que el navegador tiene prohibido almacenar la copia local (
no-store), solicita la página nuevamente al servidor web. - El middleware de autenticación detecta la ausencia de sesión y desvía al usuario al
/logincon código302.
3. Resolución Segura de Errores: Route::fallback
Por defecto en Laravel, si un usuario solicita una ruta inexistente, el framework dispara NotFoundHttpException antes de hidratar la sesión HTTP. Esto provocaría que el usuario autenticado sea tratado como invitado y pierda el acceso al layout administrativo.
Mañuti subsana esta vulnerabilidad enrutando el comodín a través de la pila de middleware completa:
// routes/web.php
Route::fallback(function () {
return response()->view('errors.404', [], 404);
})->middleware([
'web',
'check.session' // Garantiza que si la sesión fue terminada remotamente, se expulse al intruso
]);
En la capa de presentación (errors/404.blade.php), la plantilla adopta un comportamiento camaleónico:
@extends(auth()->check() ? 'layouts.backend.base' : 'layouts.blank')
@section('content')
@auth
{{-- Conserva la barra de navegación, menús y Kill Switch WebSocket activo --}}
@include('errors.content.404_admin')
@else
{{-- Muestra interfaz limpia sin filtrar estructura interna --}}
@include('errors.content.404_public')
@endauth
@endsection
4. Resiliencia de Sesiones Expiradas (Intercepción Error 419 CSRF)
Cuando una pestaña de navegación permanece ociosa más allá del tiempo de vida de la cookie, el token CSRF caduca. Para evitar la exposición de páginas de error crudas (419 Page Expired), el núcleo captura la excepción y ejecuta una redirección amigable:
// bootstrap/app.php
$exceptions->render(function (TokenMismatchException $e, Request $request) {
if ($request->wantsJson()) {
return response()->json([
'message' => 'La sesión ha expirado por inactividad.',
'error' => 'csrf_token_expired'
], 419);
}
return redirect()
->route('login')
->with('warning', 'La sesión ha expirado por inactividad. Ingrese sus credenciales nuevamente.');
});