Latte è sinonimo di sicurezza
Latte è l'unico sistema di template per PHP con una protezione efficace contro la vulnerabilità critica Cross-site Scripting (XSS). Il merito è dell'escaping sensibile al contesto. Parleremo di:
- come funziona la vulnerabilità XSS e perché è così pericolosa
- cosa rende Latte così efficace nel difendersi dall'XSS
- con quanta facilità si creano falle di sicurezza in Twig, Blade e template simili
Cross-site Scripting (XSS)
Il Cross-site Scripting (in breve XSS) è una delle vulnerabilità più diffuse e pericolose dei siti web. Permette a un aggressore di inserire uno script malevolo (malware) nella pagina di qualcun altro, che viene poi eseguito nel browser di un utente ignaro.
Cosa può fare uno script del genere? Per esempio può inviare all'aggressore qualsiasi contenuto della pagina compromessa, compresi i dati sensibili mostrati dopo il login. Può modificare la pagina o eseguire altre richieste a nome dell'utente. Se si trattasse di una webmail, per esempio, potrebbe leggere messaggi riservati, modificare il contenuto mostrato o cambiare le impostazioni, per esempio attivare l'inoltro di una copia di tutti i messaggi all'indirizzo dell'aggressore, per accedere anche alla posta futura.
Ecco perché l'XSS si colloca stabilmente tra le vulnerabilità più pericolose. Se compare in un sito, va eliminata al più presto per impedirne lo sfruttamento.
Come nasce la vulnerabilità?
L'errore si verifica dove la pagina web viene generata e le variabili vengono stampate. Immaginate di creare una pagina di ricerca che all'inizio contiene un paragrafo con il termine cercato, così:
echo '<p>Risultati della ricerca per <em>' . $search . '</em></p>';
L'aggressore può inserire nel campo di ricerca, e quindi nella variabile $search, una stringa qualsiasi, compreso
codice HTML come <script>alert("Hacked!")</script>. Poiché l'output non viene sanificato, esso entra a
far parte della pagina mostrata:
<p>Risultati della ricerca per <em><script>alert("Hacked!")</script></em></p>
Invece di mostrare la stringa cercata, il browser esegue il JavaScript. E così l'aggressore prende il controllo della pagina.
Potreste obiettare che inserire codice in una variabile esegue il JavaScript solo nel browser dell'aggressore. Come arriva alla vittima? Da questo punto di vista distinguiamo diversi tipi di XSS. Nel nostro esempio della ricerca parliamo di XSS riflesso. Qui bisogna indurre la vittima a cliccare un link che contiene il codice malevolo in un parametro:
https://example.com/?search=<script>alert("Hacked!")</script>
Convincere l'utente a cliccare il link richiede un po' di ingegneria sociale, ma non è particolarmente complicato. Gli utenti
cliccano sui link, sia nelle e-mail sia sui social, senza pensarci troppo. Il fatto che l'indirizzo contenga qualcosa di sospetto
si può mascherare con un accorciatore di URL: l'utente vede allora soltanto bit.ly/xxx.
Esiste però una seconda forma di attacco, molto più pericolosa, nota come XSS memorizzato o XSS persistente, in cui l'aggressore riesce a salvare il codice malevolo sul server, così che venga inserito automaticamente in determinate pagine.
Un esempio sono le pagine in cui gli utenti scrivono commenti. L'aggressore invia un contributo contenente codice, che viene salvato sul server. Se le pagine non sono sufficientemente protette, il codice verrà eseguito nel browser di ogni visitatore.
Potrebbe sembrare che il nocciolo dell'attacco sia far entrare nella pagina la stringa <script>. In realtà
ci sono molti modi di inserire
JavaScript. Mostriamo un esempio con un attributo HTML. Consideriamo una galleria fotografica in cui alle immagini si possono
aggiungere didascalie, mostrate nell'attributo alt:
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
All'aggressore basta inserire come didascalia una stringa costruita ad arte, " onload="alert('Hacked!'), e se
l'output non viene sanificato il codice risultante avrà questo aspetto:
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
L'attributo onload iniettato entra ora a far parte della pagina. Il browser esegue il codice che contiene non
appena l'immagine viene scaricata. Hacked!
Come difendersi dall'XSS?
Qualsiasi tentativo di individuare gli attacchi con liste nere, per esempio bloccando la stringa <script>,
è insufficiente. Il fondamento di una difesa funzionante è la sanificazione coerente di tutti i dati stampati nella
pagina.
Si tratta anzitutto di sostituire tutti i caratteri dal significato speciale con le sequenze corrispondenti, operazione nota
colloquialmente come escaping (il primo carattere della sequenza si chiama carattere di escape, da cui il nome). Per
esempio nel testo HTML il carattere < ha un significato speciale; se non deve essere interpretato come l'inizio di
un tag, dobbiamo sostituirlo con la sequenza visivamente corrispondente, l'entità HTML <. Il browser mostra
allora il segno di minore.
È fondamentale distinguere il contesto in cui i dati vengono stampati, perché le stringhe si sanificano in modo diverso in contesti diversi. In contesti diversi hanno un significato speciale caratteri diversi. Per esempio l'escaping è diverso nel testo HTML, negli attributi HTML, dentro certi elementi speciali e così via. Ne parleremo tra poco in dettaglio.
La sanificazione conviene eseguirla proprio nel momento in cui la stringa viene stampata nella pagina: così si è sicuri che venga fatta davvero, ed esattamente una volta. È meglio se della sanificazione si occupa automaticamente il sistema di template stesso, perché se non è automatica il programmatore può dimenticarsene. E una sola svista significa un sito vulnerabile.
L'XSS non riguarda però solo la stampa dei dati nei template, ma anche le altre parti dell'applicazione, che devono gestire
correttamente i dati non attendibili. Per esempio il JavaScript della vostra applicazione deve usare innerText o
textContent in relazione ai dati non attendibili, non innerHTML. Particolare attenzione va prestata alle
funzioni che valutano le stringhe come JavaScript, come eval(), ma anche setTimeout(), oppure all'uso di
setAttribute() con attributi di evento come onload e simili. Tutto questo, però, va oltre l'ambito
coperto dai template.
La difesa ideale in 3 punti:
- Riconosce il contesto in cui i dati vengono stampati.
- Sanifica i dati secondo le regole di quel contesto (è cioè “sensibile al contesto”).
- Lo fa automaticamente.
Escaping sensibile al contesto
Cosa si intende esattamente con la parola contesto? È un punto del documento con proprie regole per il trattamento dei dati stampati. Dipende dal tipo di documento (HTML, XML, CSS, JavaScript, testo semplice, …) e può variare nelle singole parti. Per esempio in un documento HTML ci sono molti punti (contesti) in cui valgono regole molto diverse. Vi sorprenderà quanti sono. Ecco i primi quattro:
<p>#testo</p>
<img src="#attributo">
<textarea>#testogrezzo</textarea>
<!-- #commento -->
Il contesto predefinito e fondamentale di una pagina HTML è il testo HTML. Quali regole valgono qui? I caratteri
< e & hanno un significato speciale, perché rappresentano l'inizio di un tag o di un'entità,
quindi dobbiamo eseguirne l'escaping sostituendoli con entità HTML (< diventa <,
& diventa &).
Il secondo contesto più frequente è il valore di un attributo HTML. Si distingue dal testo perché qui ha un significato
speciale l'apice " o ' che delimita l'attributo. Va scritto come entità, per non essere interpretato
come la fine dell'attributo. Al contrario, dentro un attributo il carattere < si può usare tranquillamente,
perché lì non ha un significato speciale: non può essere interpretato come l'inizio di un tag o di un commento. Attenzione
però: in HTML i valori degli attributi si possono scrivere anche senza apici, e in quel caso hanno un significato speciale tutta
una serie di caratteri, il che ne fa un ulteriore contesto a sé.
Vi sorprenderà, ma regole speciali valgono dentro gli elementi <textarea> e <title>,
dove il carattere < non deve (ma può) subire l'escaping, a meno che non sia seguito da /. Questo
però è più che altro un dettaglio.
Le cose si fanno interessanti dentro i commenti HTML. Qui per l'escaping non si usano le entità HTML. Anzi, nessuna specifica dice come vada eseguito l'escaping nei commenti. Bisogna solo attenersi a regole piuttosto curiose ed evitare al loro interno certe combinazioni di caratteri.
I contesti possono anche stratificarsi, cosa che avviene quando incorporiamo JavaScript o CSS nell'HTML. Lo si può fare in due modi diversi, con un elemento o con un attributo:
<script>#elemento-js</script>
<img onclick="#attributo-js">
<style>#elemento-css</style>
<p style="#attributo-css"></p>
Due strade e due modi diversi di fare l'escaping dei dati. Dentro gli elementi <script> e
<style>, come nei commenti HTML, non si esegue l'escaping con le entità HTML. Stampando dati dentro questi
elementi va rispettata una sola regola: il testo non deve contenere rispettivamente la sequenza </script o
</style.
Al contrario, negli attributi style e on*** l'escaping si esegue con le entità HTML.
E naturalmente, dentro il JavaScript o il CSS annidati, valgono le regole di escaping di quei linguaggi. Una stringa in un
attributo come onload subisce quindi prima l'escaping secondo le regole di JS e poi quello secondo le regole degli
attributi HTML.
Uff… Come vedete, l'HTML è un documento molto complesso, in cui i contesti si stratificano, e senza sapere con precisione dove state stampando i dati (cioè in quale contesto) non potete dire come farlo correttamente.
Volete un esempio?
Prendiamo la stringa Rock'n'Roll.
Se la stampate nel testo HTML, in questo caso specifico non serve alcuna sostituzione, perché la stringa non contiene caratteri dal significato speciale. La situazione cambia se la stampate dentro un attributo HTML racchiuso tra apici singoli. In tal caso dovete convertire gli apici in entità HTML:
<div title='Rock'n'Roll'></div>
Semplice. Una situazione molto più interessante nasce quando i contesti si stratificano, per esempio se la stringa fa parte di JavaScript.
Per prima cosa stampiamola dentro il JavaScript stesso. La racchiudiamo cioè tra apici e allo stesso tempo eseguiamo
l'escaping degli apici che contiene, con il carattere \:
'Rock\'n\'Roll'
Possiamo anche aggiungere una chiamata di funzione, perché il codice faccia qualcosa:
alert('Rock\'n\'Roll');
Se inseriamo questo codice in un documento HTML con <script>, non serve alcuna ulteriore modifica, perché
non contiene la sequenza vietata </script:
<script> alert('Rock\'n\'Roll'); </script>
Se invece volessimo inserirlo in un attributo HTML, dobbiamo comunque convertire gli apici in entità HTML:
<div onclick='alert('Rock\'n\'Roll')'></div>
Il contesto annidato non deve però essere per forza JS o CSS. Spesso è anche un URL. I parametri negli URL subiscono
l'escaping convertendo i caratteri dal significato speciale in sequenze che iniziano con %. Esempio:
https://example.org/?a=Jazz&b=Rock%27n%27Roll
E quando stampiamo questa stringa in un attributo, applichiamo ancora l'escaping di questo contesto e sostituiamo
& con &:
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
Se siete arrivati fin qui, complimenti: è stato faticoso. Ora avete una buona idea di cosa siano i contesti e l'escaping. E non dovete preoccuparvi che sia complicato: Latte fa tutto questo per voi, automaticamente.
Latte contro i sistemi ingenui
Abbiamo mostrato come si esegue correttamente l'escaping in un documento HTML e quanto sia essenziale conoscere il contesto, cioè il punto in cui stampiamo i dati. In altre parole, come funziona l'escaping sensibile al contesto. Benché sia un presupposto necessario per una difesa efficace contro l'XSS, Latte è l'unico sistema di template per PHP capace di farlo.
Com'è possibile, se oggi tutti i sistemi dichiarano di avere l'escaping automatico? L'escaping automatico senza conoscenza del contesto è un po' un inganno, che crea un falso senso di sicurezza.
Sistemi di template come Twig, Laravel Blade e altri non vedono nel template alcuna struttura HTML. Non vedono quindi nemmeno i contesti. Rispetto a Latte sono ciechi e ingenui. Elaborano soltanto i propri tag; tutto il resto è per loro un insignificante flusso di caratteri:
░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- nel testo: <span>{{ foo }}</span>
- nel tag: <span {{ foo }} ></span>
- nell'attributo: <span title='{{ foo }}'></span>
- nell'attributo senza apici: <span title={{ foo }}></span>
- nell'attributo con un URL: <a href="{{ foo }}"></a>
- nell'attributo con JavaScript: <img onload="{{ foo }}">
- nell'attributo con CSS: <span style="{{ foo }}"></span>
- in JavaScript: <script>var = {{ foo }}</script>
- in CSS: <style>body { content: {{ foo }}; }</style>
- nel commento: <!-- {{ foo }} -->
I sistemi ingenui si limitano a convertire meccanicamente i caratteri < > & ' " in entità HTML: un
metodo di escaping valido nella maggior parte dei casi d'uso, ma tutt'altro che sempre sufficiente. Non sono quindi in grado di
rilevare né di impedire la nascita di varie falle di sicurezza, come mostreremo qui sotto.
Latte vede il template esattamente come lo vedete voi. Comprende l'HTML e l'XML, riconosce i tag, gli attributi e così via. E grazie a questo distingue i singoli contesti e tratta i dati di conseguenza. Offre così una protezione davvero efficace contro la vulnerabilità critica Cross-site Scripting.
░░░░░░░░░░░<span>{$foo}</span>
░░░░░░░░░░<span {$foo} ></span>
░░░░░░░░░░░░░░░░<span title='{$foo}'></span>
░░░░░░░░░░░░░░░░░░░░░░░░░<span title={$foo}></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<a href="{$foo}"></a>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<img onload="{$foo}">
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<span style="{$foo}"></span>
░░░░░░░░░░░░░░░░░░<script>░░░░░░{$foo}</script>
░░░░░░░░░░<style>░░░░░░░░░░░░░░░░{$foo}░░░</style>
░░░░░░░░░░░░░░<!--░{$foo}░-->
- nel testo: <span>{$foo}</span>
- nel tag: <span {$foo} ></span>
- nell'attributo: <span title='{$foo}'></span>
- nell'attributo senza apici: <span title={$foo}></span>
- nell'attributo con un URL: <a href="{$foo}"></a>
- nell'attributo con JavaScript: <img onload="{$foo}">
- nell'attributo con CSS: <span style="{$foo}"></span>
- in JavaScript: <script>var = {$foo}</script>
- in CSS: <style>body { content: {$foo}; }</style>
- nel commento: <!-- {$foo} -->
Dimostrazione dal vivo
A sinistra vedete il template in Latte, a destra il codice HTML generato. La variabile $text viene stampata più
volte, ogni volta in un contesto leggermente diverso. E quindi con un escaping leggermente diverso. Potete modificare voi stessi
il codice del template, per esempio cambiare il contenuto della variabile. Provate:
Non è fantastico? Latte esegue automaticamente l'escaping sensibile al contesto, così il programmatore:
- non deve pensare né sapere come si esegue l'escaping e dove
- non può sbagliare
- non può dimenticarsi dell'escaping
E questi non sono nemmeno tutti i contesti che Latte distingue durante la stampa e per i quali adatta il trattamento dei dati. Passeremo ora in rassegna altri casi interessanti.
Come si bucano i sistemi ingenui
Con alcuni esempi pratici mostreremo quanto sia importante distinguere i contesti e perché i sistemi di template ingenui non offrono una protezione sufficiente contro l'XSS, a differenza di Latte. Negli esempi useremo Twig come rappresentante dei sistemi ingenui, ma lo stesso vale per gli altri sistemi.
Vulnerabilità negli attributi
Proviamo a iniettare codice malevolo nella pagina tramite un attributo HTML, come mostrato sopra. Prendiamo un template in Twig che disegna un'immagine:
<img src={{ imageFile }} alt={{ imageAlt }}>
Notate che intorno ai valori degli attributi non ci sono apici. Il programmatore potrebbe averli dimenticati: succede. In React, per esempio, il codice si scrive così, senza apici, e chi passa da un linguaggio all'altro può facilmente dimenticarli.
L'aggressore inserisce come didascalia dell'immagine una stringa costruita ad arte, foo onload=alert('Hacked!').
Sappiamo già che Twig non è in grado di stabilire se la variabile viene stampata nel flusso di testo HTML, dentro un attributo,
in un commento HTML e così via; in breve, non distingue i contesti. E si limita a convertire meccanicamente i caratteri
< > & ' " in entità HTML. Il codice risultante avrà quindi questo aspetto:
<img src=photo0145.webp alt=foo onload=alert('Hacked!')>
È nata una falla di sicurezza!
Un attributo onload falsificato è entrato a far parte della pagina e il browser lo esegue subito dopo aver
scaricato l'immagine.
Vediamo ora come tratta lo stesso template Latte:
<img src={$imageFile} alt={$imageAlt}>
Latte vede il template esattamente come lo vedete voi. A differenza di Twig comprende l'HTML e sa che la variabile viene stampata come valore di un attributo non racchiuso tra apici. Perciò li aggiunge. Quando l'aggressore inserisce la stessa didascalia, il codice risultante avrà questo aspetto:
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
Latte ha impedito con successo l'XSS.
Stampare una variabile in JavaScript
Grazie all'escaping sensibile al contesto è possibile usare in modo naturale le variabili PHP dentro JavaScript.
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
Se la variabile $movie contiene la stringa 'Amarcord & 8 1/2', verrà generato l'output seguente.
Notate il diverso escaping usato nell'HTML rispetto a quello usato in JavaScript, e un escaping ancora diverso nell'attributo
onclick:
<p onclick="alert("Amarcord & 8 1/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1/2";</script>
Controllo dei link
Latte controlla automaticamente che una variabile usata negli attributi di URL come src, href,
action, formaction o data sull'elemento <object> contenga un URL sicuro.
Ammette i protocolli comuni (http, https, ftp, mailto, tel,
sms) e gli URL relativi, mentre blocca quelli potenzialmente pericolosi come javascript:.
{var $link = 'javascript:attack()'}
<a href={$link}>cliccate qui</a>
Stampa:
<a href="">cliccate qui</a>
Il controllo si può disattivare con il filtro nocheck.
Framework lato client
Framework come Vue o Angular trattano una parte della pagina come un proprio template e interpretano le doppie parentesi
graffe {{ ... }} al suo interno come un'espressione. Se un aggressore riesce a far entrare una sequenza del genere in
una variabile stampata, il framework la valuta nel browser dopo il caricamento della pagina, anche se sul server non è avvenuto
alcun XSS.
Latte lo impedisce. Stampando nel testo HTML converte la coppia {{ in {<!-- -->{; il commento
vuoto è invisibile al lettore, ma impedisce al framework di riconoscere il proprio tag. Qualsiasi altra occorrenza del carattere
{ viene sostituita dall'entità { (dentro i valori degli attributi ogni { subisce
questo escaping).
{var $query = 'Hi {{ constructor.constructor("alert(1)")() }}'}
<h1>Ricerca: {$query}</h1>
Stampa:
<h1>Ricerca: Hi {<!-- -->{ constructor.constructor("alert(1)")() }}</h1>
La protezione funziona automaticamente e non richiede alcuna configurazione.
I limiti di Latte
Latte non è una protezione completa dall'XSS per l'intera applicazione. Ci dispiacerebbe se, usando Latte, smetteste di pensare alla sicurezza. L'obiettivo di Latte è garantire che un aggressore non possa alterare la struttura della pagina né falsificare elementi o attributi HTML. Non controlla però la correttezza del contenuto dei dati stampati, né la correttezza del comportamento del JavaScript. Questo va oltre le competenze di un sistema di template. Verificare la correttezza dei dati, soprattutto di quelli inseriti dall'utente e quindi non attendibili, è un compito importante del programmatore.
Volete mettere alla prova le vostre conoscenze sull'escaping nei singoli contesti? Provate il quiz sulla vulnerabilità XSS.