DIEGO CECATO / NOTE DAL CAMPO

Symfony 8.2: una policy sbagliata deve fallire prima del deploy

diego cecato//
Symfony 8.2 ExpressionLanguage: lint, compilazione e controllo delle policy prima del deploy

Un controllo di accesso è scritto in una stringa. Dentro c’è una variabile sbagliata, oppure una funzione che quell’espressione non conosce. Il codice PHP passa i controlli sintattici, il deploy riesce e il problema emerge soltanto quando una richiesta attraversa quella regola. Il risultato può essere un errore, una funzionalità interrotta o un comportamento diverso da quello atteso. Non è il modo in cui vorrei scoprire un difetto in una policy.

Il 2 novembre 2026 Symfony ha presentato tre cambiamenti collegati per ExpressionLanguage: compilazione delle espressioni in PHP durante il warmup, validazione anticipata e un provider che limita quali costanti possono essere lette. Sono novità previste per Symfony 8.2, ancora in sviluppo al momento dell’annuncio, non una promessa già disponibile su ogni installazione stabile.

La parte interessante non è soltanto che una valutazione possa costare meno microsecondi. È che una regola espressa come testo comincia ad avere un ciclo di vita più verificabile: può essere controllata prima di ricevere traffico, compilata dove serve e circoscritta rispetto alle risorse che può leggere. Ma questi tre vantaggi non significano che la policy sia automaticamente corretta.

Il punto debole non è la stringa. È quando la verifichiamo

ExpressionLanguage non vive soltanto in un componente isolato. Symfony la usa negli attributi #[IsGranted] dei controller, nelle regole access_control, nelle guardie dei workflow e in altri attributi. È comoda perché permette di descrivere una condizione vicino al punto in cui viene applicata. Ed è proprio questa comodità a rendere facile sottovalutare il controllo di qualità: l’espressione non è PHP normale e un controllo statico del file che la contiene non ne verifica necessariamente variabili e funzioni.

Immaginiamo una regola di accesso che intenda confrontare l’identificativo dell’utente, ma usi usr al posto di user. La configurazione può essere sintatticamente valida come YAML e sbagliata per il linguaggio delle espressioni. Finché nessuno la valuta, il difetto può rimanere nascosto. Questo è il confine su cui Symfony 8.2 interviene.

Le modifiche introdotte con le PR di validazione del warmup fanno fallire la preparazione della cache quando alcune espressioni configurate, tra cui access_control e guardie dei workflow, sono invalide. Il controllo usa il linguaggio che dovrà valutarle, non un parser generico che ignora le funzioni disponibili. È una differenza importante: un errore individuato nel processo di build o deploy costa meno di un errore scoperto dalla prima richiesta reale.

Da un albero di nodi a un metodo PHP: cosa cambia davvero

Il componente sapeva già fare caching delle espressioni analizzate. Ma conservare i nodi del parser non elimina tutto il lavoro necessario per valutarli: ad ogni esecuzione rimaneva un costo di ricostruzione e interpretazione. Le guardie dei workflow, secondo la descrizione degli sviluppatori, non beneficiavano nemmeno dello stesso caching.

Con la PR #66652 Symfony 8.2 compila le espressioni integrate nel framework in metodi PHP statici durante il warmup della cache. A runtime il percorso può diventare una chiamata a metodo invece di attraversare di nuovo l’albero dei nodi. Per una condizione come is_granted("ROLE_ADMIN"), il codice compilato delega ancora il controllo all’authorization checker: non sostituisce la decisione di sicurezza con una costante.

Nicolas Grekas riporta un ordine di grandezza di 15–30 microsecondi per il vecchio percorso e circa 1 microsecondo per la chiamata compilata. È un microbenchmark attribuito all’implementazione e al suo contesto, non una misura indipendente delle prestazioni di una richiesta HTTP. Se la pagina impiega 200 millisecondi per database, rete e rendering, non possiamo trasformare quel rapporto in un aumento equivalente della velocità complessiva.

C’è anche un costo che si sposta: la compilazione avviene durante la preparazione della cache. È un trade-off sensato quando il codice viene distribuito con cache pronta e riutilizzato molte volte. Non va però confuso con una garanzia universale per ogni istanza personalizzata di ExpressionLanguage. La compilazione automatica annunciata riguarda le integrazioni supportate da Symfony; una propria implementazione richiede di valutare separatamente come costruire, cacheare e invocare le espressioni.

In modalità debug la compilazione automatica viene saltata, così le funzioni delle espressioni possono essere modificate senza dover ricostruire la cache a ogni tentativo. Confrontare il comportamento di sviluppo e produzione senza considerare questa differenza porta facilmente a misurazioni ingannevoli.

Il comando che vorrei vedere in CI

La seconda parte è operativamente più interessante: lint:expressions. Il comando introdotto nella PR #66691 controlla anche espressioni presenti negli attributi dei controller, tra cui #[IsGranted], #[IsCsrfTokenValid] e #[Cache], oltre a quelle raccolte per la compilazione. Funziona anche in debug, quindi può entrare nella pipeline prima della distribuzione.

php bin/console lint:expressions

Questo comando non è un test di autorizzazione completo. Può segnalare un nome di variabile sconosciuto, una funzione non disponibile o un’espressione malformata. Non può stabilire da solo se il ruolo scelto sia quello corretto, se un utente debba vedere il record di un altro cliente o se una regola di business consenta un’eccezione sbagliata. Una condizione formalmente valida può essere pericolosamente permissiva.

Una policy che supera il parser non è ancora una policy corretta. La prima verifica impedisce errori banali; la seconda deve dimostrare che l’accesso concesso è proprio quello previsto.

Per questo metterei il lint accanto a test di integrazione che coprano almeno tre casi: utente autorizzato, utente non autorizzato e soggetto appartenente a un altro contesto o tenant. Non è una funzionalità automatica di Symfony: è una strategia di verifica che deriva dal problema. Chi sviluppa un’applicazione con dati di più clienti sa che il caso più importante spesso non è il 403 evidente, ma il 200 restituito alla persona sbagliata.

Le costanti sono un altro confine di fiducia

C’è poi un dettaglio che rischia di passare sotto traccia. Le funzioni constant() ed enum() permettono alle espressioni di leggere nomi che non arrivano dal normale elenco di variabili passate al valutatore. Se l’espressione proviene da una sorgente non fidata, questo amplia la superficie di lettura. Symfony 8.2 introduce ConstantFunctionProvider per consentire esplicitamente solo i nomi ammessi, con pattern che possono coprire gruppi di costanti o casi enum.

Per esempio, una regola può aver bisogno di costanti ROLE_* di una classe applicativa e non di qualsiasi costante definita dal processo. Il provider permette di descrivere quel perimetro. Le versioni senza restrizioni delle funzioni vengono deprecate per le istanze personalizzate, mentre i linguaggi costruiti dal framework mantengono una configurazione compatibile con il comportamento precedente. La guida di upgrade 8.2 documenta questa transizione.

Qui serve precisione: un elenco di costanti consentite non è una sandbox completa, non filtra automaticamente tutte le funzioni custom e non sostituisce la validazione di ciò che un utente può scrivere. È una riduzione mirata della superficie esposta. Se accetti espressioni da clienti o amministratori con privilegi limitati, devi comunque ragionare sulle funzioni registrate, sui dati in ingresso, sui tempi di esecuzione e sul contesto in cui la regola viene valutata.

Come distribuirei questa novità senza fidarmi di una spunta verde

Il percorso di adozione, una volta disponibile la versione prevista e verificate le dipendenze, dovrebbe essere deliberato. Non partirei dal benchmark. Partirei dall’inventario: dove sono le espressioni, quali proteggono accessi, quali decidono transizioni di stato e quali dipendono da funzioni registrate dall’applicazione?

  • In sviluppo: censire attributi, regole access_control e guardie dei workflow; aggiungere test sui casi consentiti e negati.
  • In CI: eseguire lint:expressions e far fallire la pipeline in presenza di errori; non usare il lint come sostituto dei test di comportamento.
  • Nel build di produzione: eseguire warmup e verificare che la compilazione avvenga senza errori; misurare anche il costo di build, non solo la request calda.
  • Per input non fidati: usare una allowlist delle costanti e rivedere tutte le funzioni disponibili alle espressioni.
  • Prima del rollout: controllare versione Symfony/PHP, deprecazioni, differenze tra debug e produzione e test di regressione sulle autorizzazioni.

Il ragionamento è vicino a quello che vale per i lock e le transazioni. Nell’articolo su Symfony 8.2 e l’attributo #[Lock] il punto era che una dichiarazione elegante non rende atomica un’operazione. Qui vale lo stesso principio: una stringa compilata non rende giusta una decisione. Serve una prova che tenga conto dello stato reale e dei confini dell’applicazione.

Il vero miglioramento è spostare il guasto

La compilazione riduce un costo tecnico concreto; il lint e la validazione anticipata riducono una categoria di sorprese operative; il provider delle costanti rende più esplicito un perimetro che prima poteva essere troppo largo. Sono tre miglioramenti diversi e complementari, non tre modi di dire che Symfony è diventato più veloce.

La misura con cui valuterei questa evoluzione è più semplice di una tabella di microsecondi: quante regole sbagliate vengono fermate prima di servire una richiesta? E quante regole formalmente valide vengono comunque esercitate nei test con utenti, risorse e permessi realistici? Il framework può aiutarci molto sulla prima domanda. La seconda resta, correttamente, responsabilità di chi progetta il sistema.

Domande frequenti

Che cosa verifica il comando lint:expressions in Symfony 8.2?

Controlla la validità delle espressioni supportate, comprese quelle in access_control, guardie dei workflow e alcuni attributi dei controller. Rileva errori di sintassi e nomi di funzioni o variabili non validi, ma non dimostra che la regola di autorizzazione sia corretta per il business.

La compilazione delle espressioni rende più veloce tutta l'applicazione?

Non necessariamente. Symfony riporta un forte miglioramento nel costo della singola valutazione, ma il guadagno complessivo dipende da quante espressioni vengono eseguite e da database, rete e altre operazioni della richiesta. Il benchmark va verificato nel proprio contesto.

Symfony 8.2 compila automaticamente anche le ExpressionLanguage create a mano?

La compilazione automatica descritta nell’annuncio riguarda le integrazioni supportate dal framework durante il cache warmup. Per istanze personalizzate bisogna valutare separatamente compilazione, cache e modalità di esecuzione.

ConstantFunctionProvider rende sicure le espressioni scritte dagli utenti?

Limita quali costanti ed enum possono essere letti dalle espressioni, ma non costituisce una sandbox completa. Restano da controllare le funzioni disponibili, i dati accessibili e il contesto di esecuzione.