Middleware Laravel 12: da Kernel.php a bootstrap/app.php con security headers, rate limiting e terminable middleware
In una piattaforma marketplace con migliaia di utenti attivi, un penetration test ha rivelato che nessuna risposta HTTP includeva Strict-Transport-Security, Content-Security-Policy o X-Content-Type-Options. L'OWASP Security Headers Cheat Sheet classifica l'assenza di security header come vulnerabilità A05:2021 (Security Misconfiguration) - la quinta voce dell'OWASP Top 10, presente nel 90% delle applicazioni testate. La soluzione: un singolo middleware che imposta tutti gli header di sicurezza, registrato globalmente in bootstrap/app.php. Il middleware HTTP implementa il Chain of Responsibility pattern - definito dal Gang of Four (1994, pp. 223) come "avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request" - e PSR-15 (PHP-FIG, accettato il 22 gennaio 2018) lo ha standardizzato con MiddlewareInterface e RequestHandlerInterface. Laravel usa una variante closure-based (Closure $next) invece dell'interfaccia PSR-15, ma il pattern architetturale è identico.
Come è cambiata la registrazione dei middleware da Laravel 10 a Laravel 12?
Il PR #6188 "Slim skeleton" di Taylor Otwell ha eliminato app/Http/Kernel.php in Laravel 11. La registrazione dei middleware - gruppi, alias, priorità - è migrata in bootstrap/app.php con un'API fluent basata sulla classe Illuminate\Foundation\Configuration\Middleware. Le release notes di Laravel 11 documentano: "Configuration that was previously done in [the HTTP Kernel] file can be done in the bootstrap/app.php file instead." In pratica, un middleware di security headers e un rate limiter personalizzato si registrano così:
/* bootstrap/app.php - registrazione middleware in Laravel 12 */
use App\Http\Middleware\SecurityHeaders;
use App\Http\Middleware\AuditLog;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Cache\RateLimiting\Limit;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
## Middleware globale: security headers su ogni risposta
$middleware->append(SecurityHeaders::class);
## Alias per middleware di rotta
$middleware->alias([
'audit' => AuditLog::class,
]);
## Rate limiting con Redis (sostituisce la config nel vecchio Kernel)
$middleware->throttleWithRedis();
})
->create();
/* app/Http/Middleware/SecurityHeaders.php - OWASP-compliant */
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);
## Header raccomandati da OWASP Security Headers Cheat Sheet
$response->headers->set('Strict-Transport-Security',
'max-age=63072000; includeSubDomains; preload');
$response->headers->set('Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy',
'geolocation=(), camera=(), microphone=()');
## Rimuovere header che espongono informazioni sul server
$response->headers->remove('X-Powered-By');
$response->headers->remove('Server');
return $response;
}
}
/* app/Http/Middleware/AuditLog.php - terminable middleware */
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class AuditLog
{
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
## Il metodo terminate() esegue DOPO l'invio della risposta al browser
## Non impatta la latenza percepita dall'utente
public function terminate(Request $request, Response $response): void
{
Log::channel('audit')->info('API access', [
'user_id' => $request->user()?->id,
'method' => $request->method(),
'path' => $request->path(),
'status' => $response->getStatusCode(),
'ip' => $request->ip(),
'duration' => defined('LARAVEL_START')
? round((microtime(true) - LARAVEL_START) * 1000) . 'ms'
: null,
]);
}
}Il middleware SecurityHeaders è un "after middleware" - esegue $next($request) prima di modificare la risposta. Il AuditLog usa il terminable middleware: il metodo terminate() esegue dopo che la risposta è stata inviata al browser (richiede FastCGI), senza impatto sulla latenza. La documentazione Laravel avverte che il container risolve un'istanza fresh per terminate - se serve la stessa istanza di handle, registrare il middleware come singleton.
Come implementare rate limiting personalizzato per API in Laravel 12?
Il rate limiting di Laravel si definisce in AppServiceProvider::boot() (in Laravel 9/10 era in RouteServiceProvider::configureRateLimiting()). La migrazione è un cambio di posizione, non di API. L'OWASP A01:2021 (Broken Access Control) - prima voce dell'OWASP Top 10, rilevata nel 94% delle applicazioni testate - raccomanda di "rate limit API and controller access to minimize the harm from automated attack tooling." Un rate limiter per piano di sottoscrizione implementa questa raccomandazione:
RateLimiter::for('api', function (Request $request) {
return match ($request->user()?->subscription_plan) {
'enterprise' => Limit::perMinute(300)->by($request->user()->id),
'pro' => Limit::perMinute(120)->by($request->user()->id),
default => Limit::perMinute(30)->by($request->ip()),
};
});La throttleWithRedis() in bootstrap/app.php sostituisce il middleware throttle con ThrottleRequestsWithRedis - usa operazioni Redis atomiche (MULTI/EXEC) per il conteggio, evitando race condition nei deployment multi-server. Senza Redis, il rate limiter usa il cache driver di default (file/database), che non è atomico e può permettere burst oltre il limite sotto carico concorrente.
Errori comuni nell'implementazione dei middleware Laravel
Il primo errore è non impostare security header. L'OWASP Secure Headers Project documenta che la maggioranza dei siti non invia Content-Security-Policy o Strict-Transport-Security. Un singolo middleware globale risolve il problema per tutte le route - ma il CSP richiede personalizzazione per applicazione (script inline, CDN, iframe embed). Un CSP troppo restrittivo rompe l'applicazione; uno troppo permissivo (unsafe-inline, unsafe-eval) vanifica la protezione.
Il secondo è usare middleware per logica che appartiene al Service Layer. Un middleware che calcola sconti, verifica giacenze o elabora ordini viola la separazione delle responsabilità. Il middleware gestisce concern cross-cutting: autenticazione, autorizzazione, header, rate limiting, logging. La logica di business va nei servizi; la validazione input nelle Form Request.
Il terzo è non testare i middleware in isolamento. Un middleware che aggiunge security header è testabile con un feature test che verifica gli header nella risposta - senza testare la logica del controller:
public function test_security_headers_are_present(): void
{
$response = $this->get('/');
$response->assertHeader('X-Content-Type-Options', 'nosniff');
$response->assertHeader('X-Frame-Options', 'DENY');
$response->assertHeader('Strict-Transport-Security');
}Il quarto è dimenticare che terminate() riceve un'istanza fresh. Se il middleware salva stato in handle() (tempo di inizio, dati della request), questi dati non sono disponibili in terminate() a meno che il middleware sia registrato come singleton nel Service Container.
La registrazione dei middleware in bootstrap/app.php è parte della ristrutturazione dello skeleton di Laravel 11/12. Insieme alla migrazione dei controller verso composizione esplicita e all'hardening applicativo, i middleware security-oriented completano il profilo di sicurezza dell'applicazione. Per conoscere il mio approccio alla sicurezza applicativa Laravel, visita la mia pagina professionale. Se le tue risposte HTTP non includono security header e il rate limiting è quello di default di Laravel, contattami per una consulenza dedicata - partiamo dall'analisi degli header con un security scan e dall'implementazione dei middleware OWASP-compliant.