Quiz: você consegue se defender da vulnerabilidade XSS?

há 2 anos De David Grudl  

Teste seus conhecimentos no quiz de segurança! Você consegue impedir que um atacante assuma o controle de uma página HTML?

Em todas as tarefas, você resolverá a mesma questão: como imprimir corretamente a variável $str em uma página HTML para que não surja uma vulnerabilidade XSS. A base da defesa é o escaping, que significa substituir caracteres com significado especial pelas sequências correspondentes. Por exemplo, ao imprimir uma string no texto HTML, onde o caractere < tem um significado especial (sinaliza o início de uma tag), substituímo-lo pela entidade HTML &lt; e o navegador exibirá corretamente o símbolo <.

Fique atento, pois a vulnerabilidade XSS é muito séria. Ela pode fazer com que um atacante assuma o controle da página ou até mesmo da conta do usuário. Boa sorte e que você consiga manter a página HTML segura!

Primeiro trio de perguntas

Indique quais caracteres e de que maneira precisam ser tratados no primeiro, segundo e terceiro exemplos:

1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>

Se a saída não fosse tratada de forma alguma, ela se tornaria parte da página exibida. Se um atacante inserisse a string 'foo" onclick="evilCode()' na variável e a saída não fosse tratada, isso faria com que, ao clicar no elemento, seu código fosse executado:

$str = 'foo" onclick="evilCode()'
❌ sem tratamento: <input value="foo" onclick="evilCode()">
✅ com tratamento:  <input value="foo&quot; onclick=&quot;evilCode()">

Solução dos exemplos individuais:

  1. os caracteres < e & representam o início de uma tag HTML e entidades, substituímo-los por &lt; e &amp;
  2. os caracteres " e & representam o fim do valor do atributo e o início de uma entidade HTML, substituímo-los por &quot; e &amp;
  3. os caracteres ' e & representam o fim do valor do atributo e o início de uma entidade HTML, substituímo-los por &apos; e &amp;

Para cada resposta correta, você ganha um ponto. Claro, em todos os três casos, é possível substituir outros caracteres por entidades também, isso não prejudica nada, mas não é necessário.

Pergunta nº 4

Continuamos. Quais caracteres precisam ser substituídos ao imprimir a variável neste contexto?

<input value=<?= $str ?>>

Solução: Como você pode ver, aqui faltam aspas. O mais fácil é simplesmente adicionar aspas e depois escapar da mesma forma que na pergunta anterior. Existe também uma segunda solução, que é substituir na string por entidades HTML o espaço e todos os caracteres que têm significado especial dentro da tag, ou seja, >, /, = e alguns outros.

Pergunta nº 5

Agora começa a ficar mais interessante. Quais caracteres precisam ser tratados neste contexto:

<script>
	let foo = '<?= $str ?>';
</script>

Solução: Dentro da tag <script>, as regras de escaping são determinadas pelo JavaScript. Entidades HTML não são usadas aqui, no entanto, aplica-se uma regra especial. Então, quais caracteres escapamos? Dentro de uma string JavaScript, escapamos obviamente o caractere ', que a delimita, usando uma barra, ou seja, substituímo-lo por \'. Como o JavaScript não suporta strings multilinha (apenas como template literal), também precisamos escapar os caracteres de fim de linha. No entanto, atenção, além dos caracteres habituais \n e \r, o JavaScript também considera os caracteres unicode \u2028 e \u2029 como fins de linha, que também devemos escapar. E, finalmente, a regra especial mencionada: a sequência </script não pode ocorrer na string. Isso pode ser evitado, por exemplo, substituindo-a por <\/script.

Se você sabia disso, parabéns.

Pergunta nº 6

O contexto a seguir parece apenas uma variação do anterior. O que você acha, o tratamento será diferente?

<p onclick="foo('<?= $str ?>')"></p>

Solução: Novamente, aplicam-se as regras de escaping em strings JavaScript, mas ao contrário do contexto anterior, onde não se escapava usando entidades HTML, aqui, pelo contrário, escapa-se. Ou seja, primeiro escapamos a string JavaScript usando barras e depois substituímos os caracteres com significado especial (" e &) por entidades HTML. Atenção, a ordem correta é importante.

Como você pode ver, o mesmo literal JavaScript pode ser codificado de forma diferente no elemento <script> e de forma diferente em um atributo!

Pergunta nº 7

Voltaremos do JavaScript de volta ao HTML. Quais caracteres devemos substituir dentro do comentário e de que maneira?

<!-- <?= $str ?> -->

Solução: dentro de um comentário HTML (e XML), todos os caracteres especiais tradicionais, como <, &, " e ', podem aparecer. O que é proibido, e isso provavelmente irá surpreendê-lo, é o par de caracteres --. O escaping desta sequência não é especificado, então cabe a você decidir como substituí-la. Você pode intercalá-los com espaços. Ou talvez substituí-los por ==.

Pergunta nº 8

Já estamos chegando ao fim, então vamos tentar variar a pergunta. Tente pensar no que é preciso ter cuidado ao imprimir a variável neste contexto:

<a href="<?= $str ?>">...</a>

Solução: além do escaping, é importante também verificar se a URL não contém um esquema perigoso como javascript:, pois uma URL construída dessa forma executaria o código do atacante ao ser clicada.

Pergunta nº 9

Para finalizar, uma pérola para os verdadeiros conhecedores. É um exemplo de uma aplicação que utiliza um framework JavaScript moderno, especificamente o Vue. Veja se você consegue pensar no que ter cuidado ao imprimir a variável dentro do elemento #app:

<div id="app">
    <?= $str ?>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const app = new Vue({
    el: '#app',
    ...
})
</script>

Este código cria uma aplicação Vue que será renderizada no elemento #app. O Vue entende o conteúdo deste elemento como seu template. E dentro do template, ele interpreta chaves duplas, que significam a impressão de uma variável ou a chamada de código javascript (por exemplo, {{ foo }}).

Portanto, dentro do elemento #app, além dos caracteres < e &, o par {{ também tem um significado especial, que devemos substituir por outra sequência correspondente para que o Vue não o interprete como sua tag. A substituição por entidades HTML, no entanto, não ajuda neste caso. Como lidar com isso? Funciona um truque: inserimos um comentário HTML vazio entre as chaves {<!-- -->{ e o Vue ignora tal sequência.

Resultados do quiz

Como você se saiu no quiz? Quantas respostas corretas você teve? Se você respondeu corretamente a pelo menos 4 perguntas, você está entre os 8% melhores solucionadores – parabéns!

No entanto, para garantir a segurança do seu site, é essencial tratar corretamente a saída em todas as situações.

Se você ficou surpreso com quantos contextos diferentes podem ocorrer em uma página HTML comum, saiba que não mencionamos nem de longe todos eles. Isso tornaria o quiz muito mais longo. No entanto, você não precisa ser um especialista em escaping em todos os contextos, se o seu sistema de template puder lidar com isso.

Então, vamos testá-los.

Como se saem os sistemas de template?

Todos os sistemas de template modernos se orgulham da função de autoescaping, que escapa automaticamente todas as variáveis impressas. Se eles fizerem isso corretamente, seu site estará seguro. Se eles fizerem isso incorretamente, o site estará exposto ao risco de vulnerabilidade XSS com todas as suas graves consequências.

Testaremos os sistemas de template populares das perguntas deste quiz para descobrir quão eficaz é o seu autoescaping. Começa o teste comparativo de sistemas de template para PHP.

Twig ❌

O primeiro da lista é o sistema de template Twig (versão 3.5), que é mais frequentemente usado em conjunto com o framework Symfony. Daremos a ele a tarefa de responder a todas as perguntas do quiz. A variável $str será sempre preenchida com uma string maliciosa e veremos como ele lida com sua impressão. Os resultados você vê à direita. Você também pode explorar suas respostas e comportamento no playground.

   {% set str = "<'\"&" %}
1) <p>{{ str }}</p>
2) <input value="{{ str }}">
3) <input value='{{ str }}'>

   {% set str = "foo onclick=evilCode()" %}
4) <input value={{ str }}>

   {% set str = "'\"\n\u{2028}" %}
5) <script>	let foo = '{{ str }}'; </script>
6) <p onclick="foo('{{ str }}')"></p>

   {% set str = "-- ---" %}
7) <!-- {{ str }} -->

   {% set str = "javascript:evilCode()" %}
8) <a href="{{ str }}">...</a>

   {% set str = "{{ foo }}" %}
9) <div id="app"> {{ str }} </div>

✅ <p>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script> let foo = &#039;&quot;u{2028}; </script>
❌ <p onclick="foo(&#039;&quot;u{2028})"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌ <div id="app"> {{ foo }} </div>

O Twig falhou em seis dos nove testes!

Infelizmente, o autoescaping automático do Twig funciona apenas no texto e atributos HTML, e ainda assim apenas quando estão entre aspas. Assim que as aspas faltam, o Twig não reporta nenhum erro e cria uma falha de segurança XSS.

Isso é particularmente problemático porque é assim que os valores dos atributos são escritos em bibliotecas populares como React ou Svelte. Um programador que usa Twig e React ao mesmo tempo pode, naturalmente, esquecer as aspas.

O autoescaping do Twig falha também em todos os outros exemplos. Nos contextos (5) e (6), é necessário escapar manualmente usando {{ str|escape('js') }}; para outros contextos, o Twig nem oferece uma função de escaping. Ele também não possui proteção contra a impressão de um link malicioso (8) ou suporte para templates Vue (9).

Blade ❌❌

O segundo participante é o sistema de template Blade (versão 10.9), que está intimamente integrado ao Laravel e seu ecossistema. Novamente, verificaremos suas capacidades em nossas perguntas do quiz. Você também pode explorar suas respostas no playground.

   @php($str = "<'\"&")
1) <p>{{ $str }}</p>
2) <input value="{{ $str }}">
3) <input value='{{ $str }}'>

   @php($str = "foo onclick=evilCode()")
4) <input value={{ $str }}>

   @php($str = "'\"\n\u{2028}")
5) <script> let foo = {{ $str }}; </script>
6) <p onclick="foo({{ $str }})"></p>

   @php($str = "-- ---")
7) <!-- {{ $str }} -->

   @php($str = "javascript:evilCode()")
8) <a href="{{ $str }}">...</a>

   @php($str = "{{ foo }}")
9) <div id="app"> {{ $str }} </div>

✅ <p>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script>	let foo = &#039;&quot; ; </script>
❌ <p onclick="foo(&#039;&quot; )"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌❌ <div id="app"> &lt;?php echo e(foo); ?&gt; </div>

O Blade falhou em seis dos nove testes!

O resultado é semelhante ao do Twig. Novamente, o autoescaping automático funciona apenas no texto e atributos HTML e apenas se estiverem entre aspas. O autoescaping do Blade falha também em todos os outros exemplos. Nos contextos (5) e (6), é necessário escapar manualmente usando {{ Js::from($str) }}. Para outros contextos, o Blade nem oferece uma função de escaping. Ele não possui proteção contra a impressão de um link malicioso (8) nem suporte para templates Vue (9).

O que é surpreendente, no entanto, é a falha da diretiva @php no Blade, que causa a impressão do próprio código PHP diretamente na saída, como você pode ver na última linha.

Smarty ❌❌❌

Agora testaremos o sistema de template mais antigo para PHP, que é o Smarty (versão 4.3). Para grande surpresa, este sistema não tem autoescaping automático ativo. Você precisa, ao imprimir variáveis, ou indicar sempre o filtro {$var|escape}, ou ativar o autoescaping HTML automático. A informação sobre isso está bastante escondida na documentação.

   {$str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>

   {$str = "foo onclick=evilCode()"}
4) <input value={$str}>

   {$str = "'\"\n\u{2028}"}
5) <script>	let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>

   {$str = "-- ---"}
7) <!-- {$str} -->

   {$str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>

   {$str = "{{ foo }}"}
9) <div id="app"> {$str} </div>

✅ <p>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script> let foo = &#039;&quot;\u2028; </script>
❌ <p onclick="foo(&#039;&quot;\u2028)"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌ <div id="app"> {{ foo }} </div>

O Smarty falhou em seis dos nove testes!

O resultado é, à primeira vista, semelhante ao das bibliotecas anteriores. O Smarty consegue escapar automaticamente apenas no texto e atributos HTML, e apenas quando os valores estão entre aspas. Em todos os outros lugares, ele falha. Nos contextos (5) e (6), é necessário escapar manualmente usando {$str|escape:javascript}. Mas isso só é possível quando o autoescaping HTML automático não está ativo, caso contrário, esses escapings entram em conflito. O Smarty é, portanto, do ponto de vista da segurança, o completo fracasso deste teste.

Latte ✅

O trio é concluído pelo sistema de modelos Latte (versão 3.0). Testaremos seu autoescapamento. Você também pode explorar suas respostas e seu comportamento no playground.

   {var $str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>

   {var $str = "foo onclick=evilCode()"}
4) <input value={$str}>

   {var $str = "'\"\n\u{2028}"}
5) <script>	let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>

   {var $str = "-- ---"}
7) <!-- {$str} -->

   {var $str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>

   {var $str = "{{ foo }}"}
9) <div id="app"> {$str} </div>

✅ <p>&lt;'"&amp;</p>
✅ <input value="&lt;&apos;&quot;&amp;">
✅ <input value='&lt;&apos;&quot;&amp;'>


✅ <input value="foo onclick=evilCode()">


✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo(&quot;&apos;\&quot;\n\u2028&quot;)"></p>


✅ <!--  - -  - - -  -->


✅ <a href="">...</a>


✅ <div id="app"> {<!-- -->{ foo }} </div>

O Latte se destacou em todas as nove tarefas!

Ele conseguiu lidar com a falta de aspas nos atributos HTML, conseguiu processar JavaScript tanto no elemento <script> quanto nos atributos e conseguiu lidar também com a sequência proibida em comentários HTML.

Além disso, evitou a situação em que clicar em um link forjado por um atacante poderia executar seu código. E conseguiu lidar com o escaping de tags para Vue.

Teste bônus

Uma das capacidades essenciais de todos os sistemas de template é o trabalho com blocos e a herança de templates associada. Portanto, tentaremos dar a todos os sistemas de template testados mais uma tarefa. Criaremos um bloco description, que imprimiremos em um atributo HTML. No mundo real, obviamente, a definição do bloco estaria em um template filho e sua impressão em um template pai, por exemplo, um layout. Esta é apenas uma forma simplificada, mas suficiente para testar o autoescaping ao imprimir blocos. Como eles se saíram?

Twig: falhou ❌ ao imprimir blocos, não trata os caracteres

{% block description %}
	rock n' roll
{% endblock %}

<meta name='description'
	content='{{ block('description') }}'>




<meta name='description'
	content=' rock n' roll '> ❌

Blade: falhou ❌ ao imprimir blocos, não trata os caracteres

@section('description')
	rock n' roll
@endsection

<meta name='description'
	content='@yield('description')'>




<meta name='description'
	content=' rock n' roll '> ❌

Latte: passou ✅ ao imprimir blocos, tratou corretamente os caracteres problemáticos

{block description}
	rock n' roll
{/block}

<meta name='description'
	content='{include description}'>




<meta name='description'
	content=' rock n&apos; roll '> ✅

Por que tantos sites são vulneráveis?

O autoescaping em sistemas como Twig, Blade ou Smarty funciona simplesmente substituindo cinco caracteres <>"'& por entidades HTML e não distingue o contexto de forma alguma. Por isso, funciona apenas em algumas situações e falha em todas as outras. O autoescaping ingênuo é uma função perigosa porque cria uma falsa sensação de segurança.

Não é surpreendente, portanto, que atualmente mais de 27% dos sites tenham vulnerabilidades críticas, principalmente XSS (fonte: Acunetix Web Vulnerability Report). Como sair disso? Usar um sistema de template que distingue contextos.

O Latte é o único sistema de template em PHP que não percebe o template apenas como uma cadeia de caracteres, mas entende HTML. Ele entende o que são tags, atributos, etc. Distingue contextos. E por isso escapa corretamente no texto HTML, de forma diferente dentro de uma tag HTML, de forma diferente dentro de JavaScript, etc.

O Latte representa, assim, o único sistema de template seguro.


Além disso, graças à sua compreensão de HTML, ele oferece os maravilhosos n:attributes, que os usuários adoram:

<ul n:if="$menu">
	<li n:foreach="$menu->getItems() as $item">{$item->title}</li>
</ul>