Middleware Laravel 12: da Kernel.php a bootstrap/app.php con security headers, rate limiting e terminable middleware

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.

Ultima modifica: