Latte é sinônimo de segurança
Latte é o único sistema de modelos PHP com proteção efetiva contra a vulnerabilidade Cross-site Scripting (XSS) crítica. Isto é graças à chamada fuga sensível ao contexto. Vamos conversar,
- qual é o princípio da vulnerabilidade do XSS e por que é tão perigoso
- o que torna o Latte tão eficaz na defesa contra o XSS
- por que Twig, Blade e outros modelos podem ser facilmente comprometidos
Roteiro transversal (XSS)
O Cross-site Scripting (XSS) é uma das vulnerabilidades mais comuns em websites e uma das mais perigosas. Ele permite que um atacante insira um script malicioso (chamado malware) em um site estrangeiro que executa no navegador de um usuário insuspeito.
O que pode fazer um roteiro desse tipo? Por exemplo, ele pode enviar conteúdo arbitrário do site comprometido para o atacante, incluindo dados sensíveis exibidos após o login. Ele pode modificar a página ou fazer outras solicitações em nome do usuário. Por exemplo, se fosse webmail, ele poderia ler mensagens sensíveis, modificar o conteúdo exibido, ou alterar configurações, por exemplo, ativar o encaminhamento de cópias de todas as mensagens para o endereço do atacante para obter acesso a futuros e-mails.
É também por isso que o XSS encabeça a lista das vulnerabilidades mais perigosas. Se uma vulnerabilidade for descoberta em um website, ela deve ser removida o mais rápido possível para evitar a exploração.
Como surge a vulnerabilidade?
O erro ocorre no local onde a página web é gerada e as variáveis são impressas. Imagine que você está criando uma página de busca, e no início haverá um parágrafo com o termo de busca no formulário:
echo '<p>Search results for <em>' . $search . '</em></p>';
Um atacante pode escrever qualquer string, incluindo código HTML, como
<script>alert("Hacked!")</script>
, no campo de busca e, portanto, na variável $search
. Como
a saída não é higienizada de forma alguma, ela se torna parte da página exibida:
<p>Search results for <em><script>alert("Hacked!")</script></em></p>
Em vez de emitir a cadeia de busca, o navegador executa o JavaScript. E assim o atacante toma conta da página.
Você pode argumentar que colocar código em uma variável de fato executará JavaScript, mas somente no navegador do atacante. Como chegar até a vítima? Desta perspectiva, podemos distinguir vários tipos de XSS. Em nossa página de busca, por exemplo, estamos falando de XSS refletidos. Neste caso, a vítima precisa ser enganada para clicar em um link que contém código malicioso no parâmetro:
https://example.com/?search=<script>alert("Hacked!")</script>
Embora seja necessária alguma engenharia social para que o usuário possa acessar o link, não é difícil. Os usuários
clicam nos links, seja em e-mails ou em mídias sociais, sem pensar muito. E o fato de que há algo suspeito no endereço pode
ser mascarado pelo encurtador de URL, de modo que o usuário só vê bit.ly/xxx
.
Entretanto, existe uma segunda e muito mais perigosa forma de ataque conhecida como XSS armazenado ou XSS persistente, onde um atacante consegue armazenar código malicioso no servidor para que ele seja automaticamente inserido em determinadas páginas.
Um exemplo disso são os websites onde os usuários postam comentários. Um atacante envia um post contendo um código e ele é salvo no servidor. Se o site não for suficientemente seguro, ele então será executado no navegador de cada visitante.
Parece que o objetivo do ataque é conseguir o <script>
fio para dentro da página. Na verdade, há muitas maneiras de incorporar
JavaScript. Tomemos um exemplo de embutir usando um atributo HTML. Vamos ter uma galeria de fotos onde você pode inserir uma
legenda para as imagens, que é impressa no atributo alt
:
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
Um atacante só precisa inserir uma string inteligentemente construída " onload="alert('Hacked!')
como um
rótulo, e se a saída não for higienizada, o código resultante terá este aspecto:
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
O falso atributo onload
torna-se agora parte da página. O navegador executará o código que ele contém assim
que a imagem for baixada. Hackeado!
Como se defender contra o XSS?
Qualquer tentativa de detectar um ataque usando uma lista negra, como o bloqueio do <script>
cordel, etc.,
são insuficientes. A base de uma defesa viável é **sanitização consistente de todos os dados impressos dentro da
página***.
Primeiro de tudo, isto envolve a substituição de todos os caracteres com significado especial por outras seqüências
correspondentes, que se chama escaping em gíria (o primeiro caractere da seqüência é chamado de caractere de fuga,
daí o nome). Por exemplo, no texto HTML, o caractere <
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 o navegador imprime um caractere.
**É muito importante distinguir o contexto no qual os dados são emitidos***. Porque contextos diferentes higienizam as cordas de forma diferente. Caracteres diferentes têm um significado especial em contextos diferentes. Por exemplo, a fuga em texto HTML, em atributos HTML, dentro de alguns elementos especiais, etc., é diferente. Discutiremos isto em detalhes em um momento.
É melhor executar a fuga diretamente quando a corda é escrita na página, assegurando que ela seja realmente feita, e feita apenas uma vez. É melhor se o processamento for tratado automaticamente diretamente pelo sistema de gabaritos. Porque se o tratamento não for feito automaticamente, o programador pode esquecê-lo. E uma omissão significa que o site é vulnerável.
Entretanto, o XSS não afeta apenas a saída de dados nos modelos, mas também outras partes da aplicação que devem tratar
adequadamente os dados não confiáveis. Por exemplo, o JavaScript em sua aplicação não deve usar innerHTML
em
conjunto com eles, mas apenas innerText
ou textContent
. Deve-se tomar cuidado especial com funções que
avaliam strings como JavaScript, que é eval()
, mas também setTimeout()
, ou usando
setAttribute()
com atributos de eventos como onload
, etc. Mas isto vai além do escopo coberto pelos
modelos.
A defesaideal de 3 pontos:
- Reconhecer o contexto no qual os dados estão sendo emitidos
- saneia os dados de acordo com as regras desse contexto (i.e. „contextual-aware“)
- faz isto automaticamente
Fugindo do Contexto
O que significa exatamente a palavra contexto? É um lugar no documento com suas próprias regras para tratar os dados a serem emitidos. Depende do tipo de documento (HTML, XML, CSS, JavaScript, texto simples, …) e pode variar em partes específicas do documento. Por exemplo, em um documento HTML, há muitos lugares (contextos) onde se aplicam regras muito diferentes. Você pode se surpreender com a quantidade de regras. Aqui estão os quatro primeiros:
<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->
O contexto inicial e básico de uma página HTML é um texto HTML. Quais são as regras aqui? Os caracteres de significado
especial <
and &
representam o início de uma tag ou entidade, portanto temos que fugir deles
substituindo-os pela entidade HTML (<
with <
, &
with
&
).
O segundo contexto mais comum é o valor de um atributo HTML. Ele difere do texto na medida em que o significado especial
aqui vai para a marca de citação "
or '
que delimita o atributo. Isto precisa ser escrito como uma
entidade para que não seja visto como o fim do atributo. Por outro lado, o caractere `<' pode ser utilizado com segurança
em um atributo porque não tem nenhum significado especial aqui; não pode ser entendido como o início de uma tag ou
comentário. Mas cuidado, em HTML você pode escrever valores de atributo sem aspas, caso em que toda uma série de caracteres tem
um significado especial, portanto é outro contexto separado.
Pode surpreendê-lo, mas aplicam-se regras especiais dentro do <textarea>
e <title>
<
character need not (but can) be escaped unless followed by /
Mas isso é mais uma curiosidade.
É interessante dentro dos comentários em HTML. Aqui, as entidades HTML não são utilizadas para escapar. Não há nenhuma especificação nem mesmo a forma de escapar nos comentários. Basta seguir as regras um tanto curiosas e evitar certas combinações de caracteres nelas.
Os contextos também podem ser estratificados, o que acontece quando incorporamos JavaScript ou CSS em HTML. Isto pode ser feito de duas maneiras diferentes, por elemento ou atributo:
<script>#js-element</script>
<img onclick="#js-attribute">
<style>#css-element</style>
<p style="#css-attribute"></p>
Duas formas e dois tipos diferentes de fuga de dados. Dentro do <script>
e <style>
elementos, como no caso dos comentários HTML, a fuga usando entidades HTML não é realizada. Ao escapar de dados dentro destes
elementos, há apenas uma regra: o texto não deve conter a seqüência </script
e </style
respectivamente.
Por outro lado, os atributos style
e on***
escapam usando entidades HTML.
E, naturalmente, dentro do JavaScript ou CSS incorporado, as regras de fuga dessas linguagens se aplicam. Assim, uma cadeia de
caracteres em um atributo como o onload
escapou primeiro de acordo com as regras do JS e depois de acordo com as
regras do atributo HTML.
Ugh… Como você pode ver, o HTML é um documento muito complexo com camadas de contextos, e sem saber exatamente onde estou produzindo os dados (ou seja, em que contexto), não há como fazê-lo corretamente.
Você quer um exemplo?
Vamos ter um fio Rock'n'Roll
.
Se você produzi-lo em texto HTML, você não precisa fazer nenhuma substituição neste caso, porque a string não contém nenhum caracter com significado especial. A situação é diferente se você escrevê-la dentro de um atributo HTML entre aspas simples. Neste caso, você precisa escapar das aspas para as entidades HTML:
<div title='Rock'n'Roll'></div>
Isto foi fácil. Uma situação muito mais interessante ocorre quando o contexto é estratificado, por exemplo, se a corda é parte do JavaScript.
Portanto, primeiro escrevemos isso no próprio JavaScript. Ou seja, nós o envolvemos entre aspas e, ao mesmo tempo, escapamos das aspas contidas nele, utilizando o caracter `\':
'Rock\'n\'Roll'
Podemos adicionar uma chamada de função para fazer o código fazer algo:
alert('Rock\'n\'Roll');
Se inserirmos este código em um documento HTML usando <script>
não precisamos modificar mais nada, porque a
seqüência proibida </script
não está presente:
<script> alert('Rock\'n\'Roll'); </script>
Entretanto, se quisermos inseri-lo em um atributo HTML, ainda precisamos escapar de citações para as entidades HTML:
<div onclick='alert('Rock\'n\'Roll')'></div>
Entretanto, o contexto aninhado não tem que ser apenas JS ou CSS. Também é comumente um URL. Parâmetros em URLs são
escapados através da conversão de caracteres especiais em seqüências que começam com %
. Exemplo:
https://example.org/?a=Jazz&b=Rock%27n%27Roll
E quando emitimos esta string em um atributo, ainda aplicamos a fuga de acordo com este contexto e substituímos
&
with &
:
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
Se você já leu até aqui, parabéns, tem sido exaustivo. Agora você tem uma boa idéia do que são contextos e fugas. E você não precisa se preocupar com o fato de ser complicado. Latte faz isso para você automaticamente.
Sistemas Latte vs Naive
Mostramos como escapar corretamente em um documento HTML e como é crucial conhecer o contexto, ou seja, onde você está produzindo os dados. Em outras palavras, como funciona a fuga sensível ao contexto. Embora este seja um pré-requisito para a defesa funcional do XSS, Latte é o único sistema de templates para PHP que faz isso.
Como isso é possível quando todos os sistemas de hoje afirmam ter fuga automática? A fuga automática sem saber o contexto é um pouco de besteira que ** cria uma falsa sensação de segurança***.
Sistemas de gabarito como Twig, Laravel Blade e outros não vêem nenhuma estrutura HTML no gabarito. Portanto, eles também não vêem contextos. Em comparação ao Latte, eles são cegos e ingênuos. Eles só lidam com sua própria marcação, tudo o mais é um fluxo de caracteres irrelevante para eles:
░░░░░░░░░░░░░░░░░{{ 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 }} -->
Sistemas ingênuos apenas convertem mecanicamente < > & ' "
caracteres em entidades HTML, o que é uma
forma válida de escapar na maioria dos usos, mas longe de sempre. Assim, eles não podem detectar ou evitar várias falhas de
segurança, como mostraremos a seguir.
Latte vê o modelo da mesma forma que você vê. Ele entende HTML, XML, reconhece tags, atributos, etc. E por causa disso, ele distingue entre contextos e trata os dados de acordo. Portanto, ele oferece uma proteção realmente eficaz contra a vulnerabilidade crítica do Cross-site Scripting.
Demonstração ao vivo
À esquerda você pode ver o template em Latte, à direita está o código HTML gerado. A variável $text
é
gerada várias vezes, cada vez em um contexto ligeiramente diferente. E, portanto, escapou um pouco diferente. Você mesmo pode
editar o código do template, por exemplo, alterar o conteúdo da variável, etc. Experimente-o:
Isso não é ótimo? Latte faz a fuga sensível ao contexto automaticamente, por isso o programador:
- não tem que pensar ou saber como escapar dos dados
- não pode estar errado
- não pode esquecê-lo
Estes não são nem mesmo todos os contextos que Latte distingue ao produzir e para os quais ele personaliza o tratamento de dados. Passaremos agora por casos mais interessantes.
Como Hackear Sistemas Naive
Vamos usar alguns exemplos práticos para mostrar como é importante a diferenciação do contexto e por que os sistemas de modelos ingênuos não oferecem proteção suficiente contra o XSS, ao contrário do Latte. Usaremos Twig como representante de um sistema ingênuo nos exemplos, mas o mesmo se aplica a outros sistemas.
Atributo Vulnerabilidade
Vamos tentar injetar código malicioso na página usando o atributo HTML, como mostramos acima. Vamos ter um template no Twig exibindo uma imagem:
<img src={{ imageFile }} alt={{ imageAlt }}>
Observe que não há citações em torno dos valores dos atributos. O codificador pode tê-los esquecido, o que simplesmente acontece. Por exemplo, em React, o código é escrito assim, sem aspas, e um codificador que está trocando de idioma pode facilmente esquecer as aspas.
O atacante insere uma corda engenhosamente construída foo onload=alert('Hacked!')
como legenda da imagem. Já
sabemos que Twig não pode dizer se uma variável está sendo impressa em um fluxo de texto HTML, dentro de um atributo, dentro de
um comentário HTML, etc.; em suma, não faz distinção entre contextos. E ele apenas converte mecanicamente
< > & ' "
caracteres em entidades HTML. Portanto, o código resultante será parecido com este:
<img src=photo0145.webp alt=foo onload=alert('Hacked!')>
** Foi criado um buraco na segurança!**
Um falso atributo onload
tornou-se parte da página e o navegador o executa imediatamente após o download da
imagem.
Agora vamos ver como o Latte lida com o mesmo modelo:
<img src={$imageFile} alt={$imageAlt}>
Latte vê o modelo da mesma forma que você vê. Ao contrário do Twig, ele entende HTML e sabe que uma variável é impressa como um valor de atributo que não está entre aspas. É por isso que ele as acrescenta. Quando um atacante insere a mesma legenda, o código resultante será parecido com este:
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
Latte impediu com sucesso o XSS.
Impressão de uma variável em JavaScript
Graças à fuga sensível ao contexto, é possível usar variáveis PHP nativamente dentro do JavaScript.
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
Se $movie
variável armazena 'Amarcord & 8 1/2'
string, ela gera a seguinte saída. Observe
diferentes fugas utilizadas em HTML e JavaScript e também no atributo onclick
:
<p onclick="alert("Amarcord & 8 1\/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1\/2";</script>
Verificação do link
Latte verifica automaticamente se a variável utilizada nos atributos src
ou href
contém uma URL da
web (ou seja, protocolo HTTP) e impede a escrita de links que possam representar um risco de segurança.
{var $link = 'javascript:attack()'}
<a href={$link}>click here</a>
Escreve:
<a href="">click here</a>
A verificação pode ser desligada utilizando um nocheck de filtro.
Limites do Latte
O Latte não é uma proteção XSS completa para toda a aplicação. Ficaríamos infelizes se você parasse para pensar em segurança ao usar o Latte. O objetivo do Latte é garantir que um atacante não possa alterar a estrutura de uma página, adulterar elementos ou atributos HTML. Mas ele não verifica a exatidão do conteúdo dos dados que estão sendo emitidos. Ou a correção do comportamento do JavaScript. Isso está além do escopo do sistema de templates. Verificar a exatidão dos dados, especialmente aqueles inseridos pelo usuário e portanto não confiáveis, é uma tarefa importante para o programador.