Latte è sinonimo di sicurezza
Latte è l'unico sistema di template PHP con una protezione efficace contro la vulnerabilità critica Cross-site Scripting (XSS). Questo grazie al cosiddetto escape sensibile al contesto. Parliamo,
- qual è il principio della vulnerabilità XSS e perché è così pericolosa
- cosa rende Latte così efficace nella difesa contro gli XSS
- perché Twig, Blade e altri template possono essere facilmente compromessi
Cross-Site Scripting (XSS)
Il Cross-Site Scripting (XSS) è una delle vulnerabilità più comuni nei siti web e molto pericolosa. Consente a un aggressore di inserire uno script dannoso (chiamato malware) in un sito straniero che viene eseguito nel browser di un utente ignaro.
Cosa può fare uno script di questo tipo? Ad esempio, può inviare all'aggressore contenuti arbitrari dal sito compromesso, compresi i dati sensibili visualizzati dopo il login. Può modificare la pagina o effettuare altre richieste per conto dell'utente. Ad esempio, se si trattasse di una webmail, potrebbe leggere i messaggi sensibili, modificare il contenuto visualizzato o cambiare le impostazioni, ad esempio attivare l'inoltro di copie di tutti i messaggi all'indirizzo dell'attaccante per ottenere l'accesso alle e-mail future.
Questo è anche il motivo per cui l'XSS è in cima alla lista delle vulnerabilità più pericolose. Se si scopre una vulnerabilità in un sito web, è necessario rimuoverla il prima possibile per impedirne lo sfruttamento.
Come nasce la vulnerabilità?
L'errore si verifica nel punto in cui viene generata la pagina web e vengono stampate le variabili. Immaginate di creare una pagina di ricerca e all'inizio ci sarà un paragrafo con il termine di ricerca nella forma:
echo '<p>Search results for <em>' . $search . '</em></p>';
Un utente malintenzionato può scrivere qualsiasi stringa, compreso il codice HTML come
<script>alert("Hacked!")</script>
nel campo di ricerca e quindi nella variabile $search
.
Poiché l'output non viene sanificato in alcun modo, diventa parte della pagina visualizzata:
<p>Search results for <em><script>alert("Hacked!")</script></em></p>
Invece di visualizzare la stringa di ricerca, il browser esegue JavaScript. In questo modo l'aggressore prende il controllo della pagina.
Si potrebbe obiettare che inserendo il codice in una variabile si esegue effettivamente JavaScript, ma solo nel browser dell'attaccante. Come arriva alla vittima? Da questo punto di vista, possiamo distinguere diversi tipi di XSS. Nell'esempio della nostra pagina di ricerca, stiamo parlando di Riflesso XSS. In questo caso, la vittima deve essere indotta a cliccare su un link che contiene codice dannoso nel parametro:
https://example.com/?search=<script>alert("Hacked!")</script>
Anche se richiede un po' di ingegneria sociale per indurre l'utente ad accedere al link, non è difficile. Gli utenti cliccano
sui link, nelle e-mail o sui social media, senza pensarci troppo. E il fatto che ci sia qualcosa di sospetto nell'indirizzo può
essere mascherato dall'accorciatore di URL, in modo che l'utente veda solo bit.ly/xxx
.
Tuttavia, esiste una seconda forma di attacco, molto più pericolosa, nota come stored XSS o persistent XSS, in cui un aggressore riesce a memorizzare il codice dannoso sul server in modo da inserirlo automaticamente in determinate pagine.
Un esempio è rappresentato dai siti web in cui gli utenti pubblicano commenti. Un aggressore invia un post contenente codice che viene salvato sul server. Se il sito non è sufficientemente sicuro, il codice viene eseguito nel browser di ogni visitatore.
Sembrerebbe che lo scopo dell'attacco sia quello di inserire la stringa nella pagina. <script>
stringa nella
pagina. In realtà, ci sono molti
modi per incorporare JavaScript. Facciamo un esempio di incorporazione tramite un attributo HTML. Abbiamo una galleria
fotografica in cui è possibile inserire una didascalia alle immagini, che viene stampata nell'attributo alt
:
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
Un aggressore deve solo inserire una stringa " onload="alert('Hacked!')
costruita in modo intelligente come
etichetta e, se l'output non viene sanificato, il codice risultante sarà simile a questo:
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
Il falso attributo onload
diventa ora parte della pagina. Il browser eseguirà il codice in esso contenuto non
appena l'immagine verrà scaricata. Hackerato!
Come difendersi dagli XSS?
Tutti i tentativi di rilevare un attacco utilizzando una blacklist, come ad esempio bloccando la stringa
<script>
ecc. sono insufficienti. La base di una difesa efficace è la sanitizzazione coerente di tutti
i dati stampati all'interno della pagina.
Innanzitutto, si tratta di sostituire tutti i caratteri con significato speciale con altre sequenze corrispondenti, il che in
gergo si chiama escaping (il primo carattere della sequenza è chiamato carattere di escape, da cui il nome). Ad esempio,
nel testo HTML, il carattere <
has a special meaning, which, if it is not to be interpreted as the beginning of a
tag, must be replaced by a visually corresponding sequence, the so-called HTML entity <
. E il browser stampa
un carattere.
È molto importante distinguere il contesto in cui i dati vengono emessi. Perché contesti diversi sanificano le stringhe in modo diverso. Diversi caratteri hanno un significato speciale in contesti diversi. Ad esempio, l'escape nel testo HTML, negli attributi HTML, all'interno di alcuni elementi speciali e così via è diverso. Ne parleremo in dettaglio tra poco.
È meglio eseguire l'escape direttamente quando la stringa viene scritta nella pagina, assicurandosi che venga effettivamente eseguito e solo una volta. È meglio se il trattamento è gestito automaticamente direttamente dal sistema di template. Perché se il trattamento non viene fatto automaticamente, il programmatore potrebbe dimenticarsene. E un'omissione significa che il sito è vulnerabile.
Tuttavia, gli XSS non riguardano solo l'output dei dati nei template, ma anche altre parti dell'applicazione che devono gestire
correttamente i dati non attendibili. Ad esempio, i JavaScript presenti nell'applicazione non devono utilizzare
innerHTML
in combinazione con essi, ma solo innerText
o textContent
. Occorre prestare
particolare attenzione alle funzioni che valutano le stringhe come JavaScript, che è eval()
, ma anche
setTimeout()
, o all'uso di setAttribute()
con attributi di eventi come onload
, ecc. Ma
questo va oltre l'ambito coperto dai template.
La difesa ideale a 3 punti:
- riconosce il contesto in cui i dati vengono emessi
- sanitizza i dati in base alle regole di quel contesto (cioè “context-aware”)
- lo fa automaticamente
Escaping consapevole del contesto
Che cosa si intende esattamente con la parola contesto? È un punto del documento con regole proprie per il trattamento dei dati da produrre. Dipende dal tipo di documento (HTML, XML, CSS, JavaScript, testo normale, …) e può variare in parti specifiche del documento. Ad esempio, in un documento HTML, ci sono molti luoghi (contesti) in cui si applicano regole molto diverse. Potreste essere sorpresi di quante ce ne siano. Ecco le prime quattro:
<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->
Il contesto iniziale e di base di una pagina HTML è il testo HTML. Quali sono le regole in questo caso? I caratteri di
significato speciale <
and &
rappresentano l'inizio di un tag o di un'entità, quindi dobbiamo
evitarli sostituendoli con l'entità HTML (<
with <
, &
with
&
).
Il secondo contesto più comune è il valore di un attributo HTML. Si differenzia dal testo perché in questo caso il
significato speciale va alla virgoletta "
or '
che delimita l'attributo. Questa deve essere scritta come
un'entità, in modo che non venga vista come la fine dell'attributo. D'altra parte, il carattere <
può essere
tranquillamente usato in un attributo perché non ha alcun significato speciale; non può essere inteso come l'inizio di un tag
o di un commento. Ma attenzione, in HTML è possibile scrivere i valori degli attributi senza virgolette, nel qual caso tutta
una serie di caratteri ha un significato speciale, quindi si tratta di un altro contesto separato.
Forse vi sorprenderà, ma all'interno dei caratteri <textarea>
e <title>
dove l'elemento
<
character need not (but can) be escaped unless followed by /
. Ma questa è più che altro una
curiosità.
È interessante all'interno dei commenti HTML. Qui, le entità HTML non vengono utilizzate per l'escape. Non esiste alcuna specifica che indichi come effettuare l'escape nei commenti. Bisogna solo seguire delle regole curiose ed evitare certe combinazioni di caratteri.
I contesti possono anche essere stratificati, il che accade quando incorporiamo JavaScript o CSS nell'HTML. Questo può essere fatto in due modi diversi, per elemento o per attributo:
<script>#js-element</script>
<img onclick="#js-attribute">
<style>#css-element</style>
<p style="#css-attribute"></p>
Due modi e due tipi diversi di escape dei dati. All'interno degli elementi <script>
e
<style>
come nel caso dei commenti HTML, non viene eseguito l'escape utilizzando le entità HTML. Quando si
esegue l'escape dei dati all'interno di questi elementi, esiste una sola regola: il testo non deve contenere la sequenza
</script
e </style
rispettivamente.
D'altra parte, gli attributi style
e on***
vengono evasi utilizzando le entità HTML.
E, naturalmente, all'interno di JavaScript o CSS incorporati, si applicano le regole di escape di quei linguaggi. Quindi, una
stringa in un attributo come onload
viene sfuggita prima secondo le regole JS e poi secondo le regole degli
attributi HTML.
Ah… Come si può vedere, l'HTML è un documento molto complesso, con diversi contesti, e senza sapere esattamente dove sto inviando i dati (cioè in quale contesto), non si può sapere come farlo correttamente.
Volete un esempio?
Prendiamo una stringa Rock'n'Roll
.
Se la si scrive in un testo HTML, in questo caso non è necessario effettuare alcuna sostituzione, perché la stringa non contiene alcun carattere con significato speciale. La situazione è diversa se la si scrive all'interno di un attributo HTML racchiuso tra apici singoli. In questo caso, è necessario eseguire l'escape delle virgolette in entità HTML:
<div title='Rock'n'Roll'></div>
Questo è facile. Una situazione molto più interessante si verifica quando il contesto è stratificato, ad esempio se la stringa fa parte di JavaScript.
Quindi, per prima cosa la scriviamo nel JavaScript stesso. Cioè, la avvolgiamo tra virgolette e allo stesso tempo facciamo
l'escape delle virgolette contenute usando il carattere \
:
'Rock\'n\'Roll'
Possiamo aggiungere una chiamata di funzione per far fare qualcosa al codice:
alert('Rock\'n\'Roll');
Se inseriamo questo codice in un documento HTML utilizzando il comando <script>
non dobbiamo modificare
nient'altro, perché la sequenza proibita </script
non è presente:
<script> alert('Rock\'n\'Roll'); </script>
Tuttavia, se vogliamo inserirlo in un attributo HTML, dobbiamo ancora eseguire l'escape delle virgolette nelle entità HTML:
<div onclick='alert('Rock\'n\'Roll')'></div>
Tuttavia, il contesto annidato non deve essere solo JS o CSS. In genere si tratta anche di un URL. I parametri negli URL
vengono evasi convertendo i caratteri speciali in sequenze che iniziano con %
. Esempio:
https://example.org/?a=Jazz&b=Rock%27n%27Roll
E quando produciamo questa stringa in un attributo, applichiamo ancora l'escape in base a questo contesto e sostituiamo
&
with &
:
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
Se siete arrivati a leggere fin qui, congratulazioni, è stato faticoso. Ora avete una buona idea di cosa siano i contesti e l'escape. E non dovete preoccuparvi che sia complicato. Latte lo fa automaticamente.
Latte contro i sistemi ingenui
Abbiamo mostrato come eseguire correttamente l'escape in un documento HTML e come sia fondamentale conoscere il contesto, cioè dove si stanno producendo i dati. In altre parole, come funziona l'escape sensibile al contesto. Sebbene questo sia un prerequisito per una difesa funzionale dagli XSS, **Latte è l'unico sistema di template per PHP che lo fa **.
Com'è possibile quando tutti i sistemi oggi affermano di avere l'escape automatico? L'escape automatico senza conoscere il contesto è una stronzata che crea un falso senso di sicurezza.
I sistemi di template come Twig, Laravel Blade e altri non vedono alcuna struttura HTML nel template. Pertanto, non vedono nemmeno i contesti. Rispetto a Latte, sono ciechi e ingenui. Gestiscono solo il proprio markup, tutto il resto è un flusso di caratteri irrilevante per loro:
░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░
░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░{{ text }}░░░░
- in text: <span>{{ text }}</span>
- in tag: <span {{ text }} ></span>
- in attribute: <span title='{{ text }}'></span>
- in unquoted attribute: <span title={{ text }}></span>
- in attribute containing URL: <a href="{{ text }}"></a>
- in attribute containing JavaScript: <img onload="{{ text }}">
- in attribute containing CSS: <span style="{{ text }}"></span>
- in JavaScriptu: <script>var = {{ text }}</script>
- in CSS: <style>body { content: {{ text }}; }</style>
- in comment: <!-- {{ text }} -->
I sistemi ingenui si limitano a convertire meccanicamente i caratteri di < > & ' "
in entità HTML, il
che è un modo valido di evadere nella maggior parte degli usi, ma non sempre. Pertanto, non sono in grado di rilevare
o prevenire varie falle di sicurezza, come mostreremo di seguito.
Latte vede il template nello stesso modo in cui lo vedete voi. Capisce l'HTML, l'XML, riconosce i tag, gli attributi, ecc. Per questo motivo, distingue i contesti e tratta i dati di conseguenza. Offre quindi una protezione davvero efficace contro la vulnerabilità critica Cross-site Scripting.
Dimostrazione dal vivo
A sinistra si vede il modello in Latte, a destra il codice HTML generato. La variabile $text
viene emessa più
volte, ogni volta in un contesto leggermente diverso. E quindi viene evasa in modo un po' diverso. È possibile modificare il
codice del modello, ad esempio cambiare il contenuto della variabile, ecc. Provate:
Non è fantastico? Latte esegue automaticamente l'escape sensibile al contesto, così il programmatore:
- non deve pensare o sapere come eseguire l'escape dei dati
- non può sbagliare
- non può dimenticarsene
Questi non sono nemmeno tutti i contesti che Latte distingue durante l'output e per i quali personalizza il trattamento dei dati. Passeremo ora in rassegna altri casi interessanti.
Come hackerare i sistemi ingenui
Utilizzeremo alcuni esempi pratici per mostrare quanto sia importante la differenziazione del contesto e perché i sistemi di template ingenui non forniscono una protezione sufficiente contro gli XSS, a differenza di Latte. Negli esempi useremo Twig come rappresentante di un sistema ingenuo, ma lo stesso vale per altri sistemi.
Vulnerabilità degli attributi
Proviamo a iniettare codice dannoso nella pagina usando l'attributo HTML come abbiamo mostrato sopra. Abbiamo un template in Twig che mostra un'immagine:
<img src={{ imageFile }} alt={{ imageAlt }}>
Si noti che non ci sono virgolette intorno ai valori degli attributi. Il codificatore potrebbe averle dimenticate, cosa che capita. Ad esempio, in React il codice è scritto così, senza virgolette, e un codificatore che cambia linguaggio può facilmente dimenticare le virgolette.
L'aggressore inserisce una stringa abilmente costruita foo onload=alert('Hacked!')
come didascalia dell'immagine.
Sappiamo già che Twig non è in grado di capire se una variabile viene stampata in un flusso di testo HTML, all'interno di un
attributo, di un commento HTML e così via; in breve, non distingue i contesti. E converte meccanicamente i caratteri di
< > & ' "
in entità HTML. Quindi il codice risultante sarà simile a questo:
<img src=photo0145.webp alt=foo onload=alert('Hacked!')>
**È stata creata una falla di sicurezza!
Un falso attributo onload
è diventato parte della pagina e il browser lo esegue immediatamente dopo aver
scaricato l'immagine.
Vediamo ora come Latte gestisce lo stesso modello:
<img src={$imageFile} alt={$imageAlt}>
Latte vede il template nello stesso modo in cui lo vedete voi. A differenza di Twig, capisce l'HTML e sa che una variabile viene stampata come valore di un attributo non tra virgolette. Ecco perché le aggiunge. Quando un utente malintenzionato inserisce la stessa didascalia, il codice risultante sarà simile a questo:
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
Latte ha impedito con successo l'XSS.
Stampare una variabile in JavaScript
Grazie all'escape sensibile al contesto, è possibile utilizzare le variabili PHP in modo nativo all'interno di JavaScript.
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
Se la variabile $movie
memorizza la stringa 'Amarcord & 8 1/2'
, genera il seguente output. Si
noti il diverso escape usato in HTML e JavaScript e anche nell'attributo onclick
:
<p onclick="alert("Amarcord & 8 1\/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1\/2";</script>
Controllo del collegamento
Latte controlla automaticamente se la variabile utilizzata negli attributi src
o href
contiene un URL
web (cioè un protocollo HTTP) e impedisce la scrittura di link che potrebbero rappresentare un rischio per la sicurezza.
{var $link = 'javascript:attack()'}
<a href={$link}>click here</a>
Scrive:
<a href="">click here</a>
Il controllo può essere disattivato utilizzando il filtro nocheck.
Limiti del Latte
Latte non è una protezione XSS completa per l'intera applicazione. Non saremmo contenti se vi fermaste a pensare alla sicurezza quando usate Latte. L'obiettivo di Latte è garantire che un aggressore non possa alterare la struttura di una pagina, manomettere elementi o attributi HTML. Ma non controlla la correttezza del contenuto dei dati in uscita. O la correttezza del comportamento di JavaScript. Questo va oltre lo scopo del sistema di template. La verifica della correttezza dei dati, soprattutto di quelli inseriti dall'utente e quindi non attendibili, è un compito importante per il programmatore.