Checklist essenziale per l'hardening di applicazioni Laravel e Symfony

Checklist essenziale per l'hardening di applicazioni Laravel e Symfony

La maggior parte delle applicazioni Laravel e Symfony che analizzo per i miei clienti ha un tratto in comune: funziona, ma non è stata hardenizzata. La configurazione è quella di default, le dipendenze non vengono auditate, gli header di sicurezza HTTP sono assenti, le sessioni non sono protette adeguatamente. Non è negligenza - è che durante lo sviluppo la priorità era il go-live, e l'hardening è rimasto "da fare dopo". Il problema è che "dopo" non arriva mai, e nel frattempo la superficie d'attacco cresce con ogni feature aggiunta.

Questa checklist nasce dalla mia esperienza diretta su decine di interventi per PMI italiane. Non è teoria - è quello che controllo quando faccio un audit, organizzato per livello: server, runtime PHP, framework, database. Per ogni punto, dove possibile, ti do la configurazione concreta. Se cerchi l'allineamento specifico alla Direttiva NIS2, ho scritto una checklist dedicata in 14 giorni. Qui il focus è più ampio: rendere la tua applicazione strutturalmente resistente.

Il server: la prima linea di difesa

L'hardening parte dal basso. Un'applicazione perfettamente codificata diventa vulnerabile se gira su un server configurato male. Queste sono le verifiche fondamentali su un VPS con Debian o Ubuntu.

Sistema operativo: installa solo i pacchetti necessari (ogni servizio in più è superficie d'attacco), configura aggiornamenti automatici di sicurezza con unattended-upgrades, abilita fail2ban per bloccare i brute-force su SSH, disabilita il login root via SSH (PermitRootLogin no), usa autenticazione a chiave - mai password.

Web server: la configurazione di Nginx o Apache è dove molti lasciano punti aperti. Ecco un blocco di header di sicurezza HTTP che applico su ogni progetto:

server {
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; frame-ancestors 'self';" always;

    server_tokens off;
}

Ogni header ha un ruolo preciso: HSTS forza HTTPS e previene downgrade attacks, X-Content-Type-Options blocca il MIME sniffing, X-Frame-Options previene il clickjacking, CSP limita le sorgenti di contenuto eseguibile. Il server_tokens off nasconde la versione di Nginx - non è sicurezza reale, ma riduce il rumore nelle scansioni automatizzate. Per Apache, gli stessi header vanno in un blocco <IfModule mod_headers.c> nel VirtualHost.

Oltre agli header: forza sempre HTTPS con redirect 301 da HTTP, disabilita i metodi HTTP non necessari (TRACE, OPTIONS su route non-API), e limita la dimensione del body delle richieste con client_max_body_size per prevenire abusi su upload.

Il runtime PHP: configurazione da produzione

Il php.ini di default è pensato per lo sviluppo, non per la produzione. Queste sono le direttive che cambio sempre:

expose_php = Off
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log

disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_multi_exec
allow_url_fopen = Off
allow_url_include = Off

session.cookie_httponly = On
session.cookie_secure = On
session.cookie_samesite = Lax
session.use_strict_mode = On
session.gc_maxlifetime = 3600

memory_limit = 256M
max_execution_time = 30
upload_max_filesize = 10M
post_max_size = 12M
max_input_vars = 1000

open_basedir = /var/www/app:/tmp

Nota su disable_functions: se la tua applicazione usa Artisan in produzione (queue worker, scheduler), exec e proc_open non possono essere disabilitate nel contesto CLI. La soluzione è usare due php.ini separati - uno per php-fpm (restrittivo) e uno per php-cli (necessario per Artisan). Questo è un dettaglio che molti tutorial ignorano e che genera errori misteriosi in produzione.

L'open_basedir è sottovalutato ma potente: limita l'accesso filesystem di PHP alle sole directory necessarie. Se un attaccante riesce a eseguire codice (tramite una vulnerabilità RCE o un upload malevolo), non potrà leggere /etc/passwd o navigare fuori dalla directory dell'applicazione.

Laravel: oltre i default

Laravel 13 offre una base solida - Blade fa auto-escaping, il CSRF token è attivo di default, Eloquent usa query parametrizzate. Ma i default non bastano. Ecco cosa configuro sempre:

// config/session.php - sessioni sicure
'driver' => env('SESSION_DRIVER', 'redis'),    // mai 'file' in produzione multi-server
'lifetime' => 60,                               // 60 minuti, non 120
'expire_on_close' => false,
'encrypt' => true,                              // crittografa il payload della sessione
'secure' => true,                               // cookie solo su HTTPS
'http_only' => true,                            // inaccessibile da JavaScript
'same_site' => 'lax',

// AppServiceProvider::boot() - rate limiting personalizzato
RateLimiter::for('login', function (Request $request) {
    return Limit::perMinute(5)->by($request->ip());
});
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

Checklist rapida Laravel:

  • APP_DEBUG=false e APP_ENV=production - sembra ovvio, ma trovo il debug attivo in produzione più spesso di quanto vorrei ammettere
  • APP_KEY generata e mai committata nel version control (il .env è in .gitignore, il .env.example non contiene la chiave reale)

- Validation rules su ogni input - non fidarti mai dei dati utente, nemmeno degli ID nelle URL ($request->validate(['id' => 'required|integer|exists:users,id']))

  • Policies e Gates per autorizzazione granulare - "l'utente è autenticato" non basta, serve "l'utente può modificare QUESTA risorsa"
  • composer audit nella pipeline CI - blocca il deploy se ci sono vulnerabilità note nelle dipendenze
  • Non usare {!! $html !!} in Blade se non su contenuto che hai sanitizzato tu con HTML Purifier
  • Se esponi API, Sanctum con rate limiting. Se hai bisogno di OAuth2 completo, Passport
  • Configura i Trusted Proxies se sei dietro un load balancer - altrimenti $request->ip() restituisce l'IP del proxy, non del client, e il rate limiting non funziona

Symfony: hardening specifico del framework

Symfony 8 ha un'architettura di sicurezza più esplicita rispetto a Laravel - meno "magic", più configurazione dichiarativa. Questo è un vantaggio per l'hardening: ogni comportamento è visibile e auditabile.

security:                                        # security.yaml
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    firewalls:
        main:
            lazy: true
            login_throttling:
                max_attempts: 5
                interval: '15 minutes'
            remember_me:
                secret: '%kernel.secret%'
                secure: true
                httponly: true
                samesite: lax
    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/api, roles: ROLE_API_USER }

framework:                                       # framework.yaml
    session:
        handler_id: '%env(REDIS_URL)%'
        cookie_secure: true
        cookie_httponly: true
        cookie_samesite: lax
        gc_maxlifetime: 3600

Checklist rapida Symfony:

  • Sistema dei Secrets per gestire credenziali in produzione - mai variabili d'ambiente in chiaro nei file di deploy
  • APP_DEBUG=0 e APP_ENV=prod - e verifica che la toolbar di debug non sia accessibile
  • Validator Component con constraint specifici su ogni DTO/form - non validare "a mano" con if/else
  • Voters per autorizzazione basata su attributi - i ruoli da soli non bastano per logiche tipo "l'utente può vedere solo i propri ordini"
  • RateLimiter sugli endpoint di autenticazione e sulle API

- Twig: non usare il filtro |raw su input utente. Mai.

  • composer audit o symfony security:check nel CI
  • Se esponi API (API Platform o custom), JWT con LexikJWTAuthenticationBundle + rate limiting + autorizzazione per risorsa

Il database: l'ultimo anello della catena

Il database è dove risiedono i dati che contano. Le regole fondamentali:

  • Utente dedicato con permessi minimi: l'applicazione NON si connette come root. Crea un utente con solo SELECT, INSERT, UPDATE, DELETE sulle tabelle necessarie - niente DROP, niente CREATE, niente GRANT
  • Binding su localhost: MySQL/PostgreSQL devono ascoltare solo su 127.0.0.1, mai su 0.0.0.0. Se serve accesso remoto, usa un tunnel SSH
  • Credenziali forti e uniche: una password diversa per ogni applicazione, generata con almeno 32 caratteri casuali
  • Backup testati: un backup che non hai mai provato a ripristinare non è un backup. Ho scritto una guida completa sul disaster recovery - il backup è solo metà del lavoro

Se usi Redis come cache o session store (e dovresti, in produzione): requirepass con password forte, binding su 127.0.0.1, e rename-command FLUSHALL "" per disabilitare i comandi distruttivi. Un Redis esposto su Internet senza password è un invito a nozze - è uno dei vettori di attacco più banali e più sfruttati che vedo nelle mie analisi.

Quanto costa non fare hardening?

Secondo l'OWASP Top 10:2025, Security Misconfiguration (A02) è la seconda causa più comune di vulnerabilità nelle applicazioni web. Non è un bug nel codice - è una configurazione mancante. È esattamente quello che questa checklist affronta.

Il costo di un incidente per una PMI italiana è tra i €40.000 e i €100.000 (fonte: ENISA Threat Landscape 2024), tra fermo operativo, ripristino, notifica GDPR e danno reputazionale. Il costo dell'hardening, su un'applicazione tipica, è 2-4 giornate di lavoro - una frazione del potenziale danno.

Con la Direttiva NIS2 in vigore dal 17 ottobre 2024, l'hardening non è più una best practice opzionale: è un requisito con sanzioni reali. Se devi allinearti alla NIS2 e non sai da dove partire, la mia checklist NIS2-ready per Laravel e Symfony ti dà un piano in 14 giorni.

Per i server che ospitano questi applicativi, raccomando sempre un provider europeo con data center in Germania o Finlandia per la conformità GDPR. Per nuovi clienti Hetzner, puoi ottenere €20.00 di credito gratuito utilizzando questo link con codice sconto - una scelta che combina prestazioni, prezzo e compliance.

In sintesi:

  • L'hardening parte dal server (OS, web server, PHP) e arriva fino al framework e al database - ogni livello ha configurazioni specifiche da verificare.
  • I default di Laravel e Symfony sono un buon punto di partenza, ma non sono sufficienti per la produzione: sessioni, rate limiting, CSP e validazione richiedono configurazione esplicita.
  • Zero code block in una configurazione di sicurezza = zero verificabilità. Ogni impostazione deve essere concreta e testabile.
  • Il costo dell'hardening (2-4 giorni) è una frazione del costo di un incidente (€40k-€100k). Con NIS2, è anche un obbligo.
  • composer audit nel CI è il singolo intervento con il miglior rapporto costo/beneficio: 5 minuti di setup, protezione continua dalle vulnerabilità note.

Se la tua applicazione Laravel o Symfony è in produzione senza un audit di sicurezza, il primo passo è capire dove sei esposto. Contattami per una valutazione - oppure dai un'occhiata al mio profilo per capire come lavoro. Ho descritto il mio approccio all'audit tecnico nella guida ai primi 30 giorni su un progetto legacy e le azioni da intraprendere quando un incidente è già in corso.

Ultima modifica: