Latte é sinônimo de segurança

O Latte é o único sistema de template para PHP com proteção eficaz contra a vulnerabilidade crítica Cross-site Scripting (XSS). E isso graças ao chamado escaping sensível ao contexto. Vamos discutir:

  • qual é o princípio da vulnerabilidade XSS e por que ela é tão perigosa
  • por que o Latte é tão eficaz na defesa contra XSS
  • como é fácil criar uma falha de segurança em templates Twig, Blade e outros

Cross-site Scripting (XSS)

Cross-site Scripting (abreviado como XSS) é uma das vulnerabilidades mais comuns de sites e, ao mesmo tempo, muito perigosa. Permite que um atacante insira um script malicioso (o chamado malware) na página de outra pessoa, que será executado no navegador de um usuário desavisado.

O que tudo um script desses pode fazer? Pode, por exemplo, enviar qualquer conteúdo da página invadida para o atacante, incluindo dados sensíveis exibidos após o login. Pode modificar a página ou realizar outras requisições em nome do usuário. Se fosse, por exemplo, um webmail, ele poderia ler mensagens sensíveis, modificar o conteúdo exibido ou redefinir a configuração, por exemplo, ativar o encaminhamento de cópias de todas as mensagens para o endereço do atacante, para obter acesso também a emails futuros.

Por isso, o XSS figura nos primeiros lugares dos rankings das vulnerabilidades mais perigosas. Se uma vulnerabilidade aparecer em um site, é necessário removê-la o mais rápido possível para evitar abusos.

Como a vulnerabilidade surge?

O erro surge no local onde a página da web é gerada e as variáveis são exibidas. Imagine que você está criando uma página de pesquisa e, no início, haverá um parágrafo com o termo pesquisado na forma de:

echo '<p>Resultados da pesquisa para <em>' . $search . '</em></p>';

Um atacante pode inserir na caixa de pesquisa e, consequentemente, na variável $search, qualquer string, ou seja, também código HTML como <script>alert("Hacked!")</script>. Como a saída não é tratada de forma alguma, ela se torna parte da página exibida:

<p>Resultados da pesquisa para <em><script>alert("Hacked!")</script></em></p>

O navegador, em vez de exibir a string pesquisada, executa o JavaScript. E assim o atacante assume o controle da página.

Você pode argumentar que, ao inserir o código na variável, o JavaScript será executado, mas apenas no navegador do atacante. Como ele chega à vítima? Deste ponto de vista, distinguimos vários tipos de XSS. Em nosso exemplo de pesquisa, estamos falando de reflected XSS. Aqui, ainda é necessário induzir a vítima a clicar em um link que conterá o código malicioso no parâmetro:

https://example.com/?search=<script>alert("Hacked!")</script>

Induzir o usuário a clicar no link requer alguma engenharia social, mas não é nada complicado. Os usuários clicam em links, seja em emails ou em redes sociais, sem pensar muito. E que há algo suspeito no endereço pode ser mascarado usando um encurtador de URL, o usuário então vê apenas bit.ly/xxx.

No entanto, existe também uma segunda forma de ataque, muito mais perigosa, conhecida como stored XSS ou persistent XSS, onde o atacante consegue armazenar o código malicioso no servidor de forma que ele seja automaticamente inserido em algumas páginas.

Um exemplo são as páginas onde os usuários escrevem comentários. O atacante envia uma postagem contendo o código e ele é armazenado no servidor. Se as páginas não estiverem suficientemente seguras, ele será executado no navegador de cada visitante.

Pode parecer que o núcleo do ataque consiste em inserir a string <script> na página. Na verdade, há muitas maneiras de inserir JavaScript. Vamos mostrar, por exemplo, a inserção através de um atributo HTML. Tenhamos uma galeria de fotos onde é possível inserir legendas para as imagens, que são exibidas no atributo alt:

echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';

Basta ao atacante inserir como legenda uma string habilmente construída " onload="alert('Hacked!') e, se a exibição não for tratada, o código resultante será assim:

<img src="photo0145.webp" alt="" onload="alert('Hacked!')">

O atributo forjado onload agora faz parte da página. O navegador executará o código contido nele assim que a imagem for baixada. Hackeado!

Como se defender do XSS?

Quaisquer tentativas de detectar ataques usando uma blacklist, como bloquear a string <script>, etc., são insuficientes. A base de uma defesa funcional é a sanitização consistente de todos os dados exibidos dentro da página.

Principalmente, trata-se de substituir todos os caracteres com significado especial por outras sequências correspondentes, o que é coloquialmente chamado de escaping (o primeiro caractere da sequência é chamado de caractere de escape, daí o nome). Por exemplo, no texto HTML, o caractere < tem um significado especial, que, quando não deve ser interpretado como o início de uma tag, deve ser substituído por uma sequência visualmente correspondente, a chamada entidade HTML &lt;. E o navegador exibirá o sinal de menor.

É muito importante distinguir o contexto em que os dados são exibidos. Porque em contextos diferentes, as strings são sanitizadas de maneiras diferentes. Em contextos diferentes, caracteres diferentes têm significado especial. Por exemplo, o escaping difere no texto HTML, nos atributos HTML, dentro de alguns elementos especiais, etc. Discutiremos isso em detalhes em breve.

O tratamento é melhor realizado diretamente ao exibir a string na página, garantindo assim que ele seja realmente feito e feito exatamente uma vez. O melhor é se o tratamento for fornecido automaticamente pelo próprio sistema de template. Porque se o tratamento não ocorrer automaticamente, o programador pode esquecê-lo. E um esquecimento significa que o site está vulnerável.

No entanto, o XSS não se refere apenas à exibição de dados em templates, mas também a outras partes do aplicativo, que devem lidar corretamente com dados não confiáveis. Por exemplo, é necessário que o JavaScript em seu aplicativo não use innerHTML em conexão com eles, mas apenas innerText ou textContent. É preciso ter cuidado especial com funções que avaliam strings como JavaScript, que é eval(), mas também setTimeout(), ou o uso da função setAttribute() com atributos de evento como onload, etc. Mas isso já está fora da área coberta pelos templates.

A defesa ideal em 3 pontos:

  1. reconhece o contexto em que os dados são exibidos
  2. sanitiza os dados de acordo com as regras do contexto dado (ou seja, “sensível ao contexto”)
  3. faz isso automaticamente

Escaping sensível ao contexto

O que exatamente se entende pela palavra contexto? É um local no documento com suas próprias regras para o tratamento dos dados exibidos. Depende do tipo de documento (HTML, XML, CSS, JavaScript, texto simples, …) e pode diferir em suas partes específicas. Por exemplo, em um documento HTML, existem muitos desses locais (contextos), onde regras muito diferentes se aplicam. Você pode se surpreender com quantos existem. Aqui temos os quatro primeiros:

<p>#texto</p>
<img src="#atributo">
<textarea>#rawtext</textarea>
<!-- #comentário -->

O contexto padrão e básico de uma página HTML é o texto HTML. Quais regras se aplicam aqui? Os caracteres < e & têm significado especial, representando o início de uma tag ou entidade, então devemos escapá-los, substituindo-os pela entidade HTML (< por &lt;, & por &amp).

O segundo contexto mais comum é o valor de um atributo HTML. Difere do texto porque aqui as aspas " ou ', que delimitam o atributo, têm significado especial. Elas precisam ser escritas como entidades para não serem entendidas como o fim do atributo. Por outro lado, no atributo, o caractere < pode ser usado com segurança, pois aqui ele não tem significado especial, não pode ser entendido como o início de uma tag ou comentário. Mas atenção, em HTML é possível escrever valores de atributos sem aspas, nesse caso, toda uma série de caracteres tem significado especial, sendo, portanto, outro contexto separado.

Você pode se surpreender, mas regras especiais se aplicam dentro dos elementos <textarea> e <title>, onde o caractere < não precisa (mas pode) ser escapado, se não for seguido por /. Mas isso é mais uma curiosidade.

É interessante dentro dos comentários HTML. Aqui, o escaping não usa entidades HTML. Na verdade, nenhuma especificação indica como o escaping deve ser feito em comentários. É apenas necessário seguir regras um tanto curiosas e evitar certas combinações de caracteres neles.

Os contextos também podem ser aninhados, o que ocorre quando inserimos JavaScript ou CSS em HTML. Isso pode ser feito de duas maneiras diferentes, por elemento e por atributo:

<script>#js-elemento</script>
<img onclick="#js-atributo">

<style>#css-elemento</style>
<p style="#css-atributo"></p>

Dois caminhos e duas maneiras diferentes de escapar dados. Dentro dos elementos <script> e <style>, assim como no caso dos comentários HTML, o escaping usando entidades HTML não é realizado. Ao exibir dados dentro desses elementos, é necessário seguir uma única regra: o texto não deve conter a sequência </script ou </style.

Por outro lado, nos atributos style e on***, o escaping é feito usando entidades HTML.

E, claro, dentro do JavaScript ou CSS aninhado, aplicam-se as regras de escaping dessas linguagens. Assim, uma string em um atributo, por exemplo, onload, é primeiro escapada de acordo com as regras do JS e depois de acordo com as regras do atributo HTML.

Ufa… Como você pode ver, HTML é um documento muito complexo, onde os contextos se aninham, e sem perceber onde exatamente os dados estão sendo exibidos (ou seja, em qual contexto), não é possível dizer como fazê-lo corretamente.

Quer um exemplo?

Tenhamos a string Rock'n'Roll.

Se você a exibir em texto HTML, neste caso específico não é necessário fazer nenhuma substituição, pois a string não contém nenhum caractere com significado especial. A situação muda se você a exibir dentro de um atributo HTML delimitado por aspas simples. Nesse caso, é necessário escapar as aspas para entidades HTML:

<div title='Rock&apos;n&apos;Roll'></div>

Isso foi simples. Uma situação muito mais interessante ocorre ao aninhar contextos, por exemplo, se a string fizer parte de JavaScript.

Primeiro, vamos exibi-la no próprio JavaScript. Ou seja, envolvemo-la em aspas e, ao mesmo tempo, escapamos as aspas contidas nela usando o caractere \:

'Rock\'n\'Roll'

Ainda podemos adicionar uma chamada de alguma função, para que o código faça algo:

alert('Rock\'n\'Roll');

Se inserirmos este código em um documento HTML usando <script>, não é necessário modificar mais nada, pois ele não contém a sequência proibida </script:

<script> alert('Rock\'n\'Roll'); </script>

No entanto, se quiséssemos inseri-lo em um atributo HTML, ainda precisaríamos escapar as aspas para entidades HTML:

<div onclick='alert(&apos;Rock\&apos;n\&apos;Roll&apos;)'></div>

Mas o contexto aninhado não precisa ser apenas JS ou CSS. Comumente, também é uma URL. Parâmetros em URLs são escapados convertendo caracteres com significado especial em sequências que começam com %. Exemplo:

https://example.org/?a=Jazz&b=Rock%27n%27Roll

E quando exibimos esta string em um atributo, ainda aplicamos o escaping de acordo com este contexto e substituímos & por &amp:

<a href="https://example.org/?a=Jazz&amp;b=Rock%27n%27Roll">

Se você leu até aqui, parabéns, foi exaustivo. Agora você tem uma boa ideia do que são contextos e escaping. E não precisa se preocupar que seja complicado. O Latte faz isso automaticamente para você.

Latte vs sistemas ingênuos

Mostramos como escapar corretamente em um documento HTML e como é crucial o conhecimento do contexto, ou seja, o local onde exibimos os dados. Em outras palavras, como funciona o escaping sensível ao contexto. Embora seja um pré-requisito necessário para uma defesa funcional contra XSS, o Latte é o único sistema de template para PHP que faz isso.

Como isso é possível, quando todos os sistemas hoje afirmam ter escaping automático? O escaping automático sem conhecimento do contexto é um pouco bullshit, que cria uma falsa sensação de segurança.

Sistemas de template, como Twig, Laravel Blade e outros, não veem nenhuma estrutura HTML no template. Portanto, eles também não veem contextos. Em comparação com o Latte, eles são cegos e ingênuos. Eles processam apenas suas próprias tags, todo o resto é para eles um fluxo de caracteres irrelevante:

░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- no texto: <span>{{ foo }}</span>
- na tag: <span {{ foo }} ></span>
- no atributo: <span title='{{ foo }}'></span>
- no atributo sem aspas: <span title={{ foo }}></span>
- no atributo contendo URL: <a href="{{ foo }}"></a>
- no atributo contendo JavaScript: <img onload="{{ foo }}">
- no atributo contendo CSS: <span style="{{ foo }}"></span>
- em JavaScript: <script>var = {{ foo }}</script>
- em CSS: <style>body { content: {{ foo }}; }</style>
- no comentário: <!-- {{ foo }} -->

Sistemas ingênuos apenas convertem mecanicamente os caracteres < > & ' " em entidades HTML, o que, embora seja um método de escaping válido na maioria dos casos de uso, está longe de ser sempre o caso. Eles não podem, portanto, detectar nem prevenir a criação de várias falhas de segurança, como mostraremos a seguir.

O Latte vê o template da mesma forma que você. Entende HTML, XML, reconhece tags, atributos, etc. E graças a isso, distingue contextos individuais e trata os dados de acordo com eles. Oferece, assim, uma proteção verdadeiramente eficaz contra a vulnerabilidade crítica 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}░-->
- no texto: <span>{$foo}</span>
- na tag: <span {$foo} ></span>
- no atributo: <span title='{$foo}'></span>
- no atributo sem aspas: <span title={$foo}></span>
- no atributo contendo URL: <a href="{$foo}"></a>
- no atributo contendo JavaScript: <img onload="{$foo}">
- no atributo contendo CSS: <span style="{$foo}"></span>
- em JavaScript: <script>var = {$foo}</script>
- em CSS: <style>body { content: {$foo}; }</style>
- no comentário: <!-- {$foo} -->

Demonstração ao vivo

À esquerda, você vê o template em Latte, à direita está o código HTML gerado. A variável $text é exibida várias vezes aqui, e cada vez em um contexto ligeiramente diferente. E, portanto, também escapada de forma ligeiramente diferente. Você pode editar o código do template você mesmo, por exemplo, alterar o conteúdo da variável, etc. Experimente:

{* TENTE EDITAR ESTE TEMPLATE *}
{var $text = "Rock'n'Roll"}
- <span>{$text}</span>
- <span title='{$text}'></span>
- <span title={$text}></span>
- <img onload="{$text}">
- <script>var = {$text}</script>
- <!-- {$text} -->
- <span>Rock'n'Roll</span>
- <span title='Rock&apos;n&apos;Roll'></span>
- <span title="Rock&apos;n&apos;Roll"></span>
- <img onload="&quot;Rock&apos;n&apos;Roll&quot;">
- <script>var = "Rock'n'Roll"</script>
- <!-- Rock'n'Roll -->

Não é ótimo! O Latte faz o escaping sensível ao contexto automaticamente, então o programador:

  • não precisa pensar nem saber como escapar em cada lugar
  • não pode errar
  • não pode esquecer de escapar

Estes nem são todos os contextos que o Latte distingue ao exibir e para os quais adapta o tratamento dos dados. Vamos percorrer outros casos interessantes agora.

Como hackear sistemas ingênuos

Em vários exemplos práticos, mostraremos como a distinção de contextos é importante e por que sistemas de template ingênuos não fornecem proteção suficiente contra XSS, ao contrário do Latte. Como representante de um sistema ingênuo, usaremos o Twig nos exemplos, mas o mesmo se aplica a outros sistemas.

Vulnerabilidade por atributo

Tentaremos injetar código malicioso na página usando um atributo HTML, como mostramos acima. Tenhamos um template em Twig renderizando uma imagem:

<img src={{ imageFile }} alt={{ imageAlt }}>

Observe que não há aspas em torno dos valores dos atributos. O codificador pode tê-las esquecido, o que simplesmente acontece. Por exemplo, em React, o código é escrito assim, sem aspas, e um codificador que alterna entre linguagens pode facilmente esquecer as aspas.

Um atacante insere como legenda da imagem uma string habilmente construída foo onload=alert('Hacked!'). Já sabemos que o Twig não pode saber se a variável está sendo exibida no fluxo de texto HTML, dentro de um atributo, comentário HTML, etc., em suma, não distingue contextos. E apenas converte mecanicamente os caracteres < > & ' " em entidades HTML. Então, o código resultante será assim:

<img src=photo0145.webp alt=foo onload=alert(&#039;Hacked!&#039;)>

E uma falha de segurança foi criada!

O atributo forjado onload tornou-se parte da página e o navegador o executará imediatamente após o download da imagem.

Agora veremos como o Latte lida com o mesmo template:

<img src={$imageFile} alt={$imageAlt}>

O Latte vê o template da mesma forma que você. Ao contrário do Twig, ele entende HTML e sabe que a variável está sendo exibida como o valor de um atributo que não está entre aspas. Portanto, ele as adiciona. Quando o atacante insere a mesma legenda, o código resultante será assim:

<img src="photo0145.webp" alt="foo onload=alert(&apos;Hacked!&apos;)">

O Latte preveniu com sucesso o XSS.

Exibição de variável em JavaScript

Graças ao escaping sensível ao contexto, é totalmente nativo usar variáveis PHP dentro de JavaScript.

<p onclick="alert({$movie})">{$movie}</p>

<script>var movie = {$movie};</script>

Se a variável $movie contiver a string 'Amarcord & 8 1/2', a seguinte saída será gerada. Observe que dentro do HTML é usado um escaping diferente do que dentro do JavaScript e ainda diferente no atributo onclick:

<p onclick="alert(&quot;Amarcord &amp; 8 1\/2&quot;)">Amarcord &amp; 8 1/2</p>

<script>var movie = "Amarcord & 8 1\/2";</script>

O Latte verifica automaticamente se a variável usada nos atributos src ou href contém uma URL da web (ou seja, protocolo HTTP) e evita a exibição de links que podem representar um risco de segurança.

{var $link = 'javascript:attack()'}

<a href={$link}>clique</a>

Exibe:

<a href="">clique</a>

A verificação pode ser desativada usando o filtro nocheck.

Limites do Latte

O Latte não é uma proteção totalmente completa contra XSS para toda a aplicação. Não gostaríamos que você parasse de pensar em segurança ao usar o Latte. O objetivo do Latte é garantir que um atacante não possa modificar a estrutura da página, forjar elementos ou atributos HTML. Mas ele não verifica a correção do conteúdo dos dados exibidos. Ou a correção do comportamento do JavaScript. Isso já está fora da competência do sistema de template. A verificação da correção dos dados, especialmente aqueles inseridos pelo usuário e, portanto, não confiáveis, é uma tarefa importante do programador.

versão: 3.0