Questionário: Você consegue se defender contra a vulnerabilidade XSS?

há um ano De David Grudl  

Teste seus conhecimentos e habilidades de segurança neste teste! Você consegue impedir que um invasor assuma o controle de uma página HTML?

Em todas as tarefas, você abordará a mesma questão: como exibir corretamente a variável $str em uma página HTML sem criar uma vulnerabilidade XSS. A base da defesa é o escaping, que significa substituir caracteres com significados especiais por sequências correspondentes. Por exemplo, ao enviar uma cadeia de caracteres para texto HTML, na qual o caractere < tem um significado especial (indicando o início de uma tag), nós o substituímos pela entidade HTML &lt;, e o navegador exibe corretamente o símbolo <.

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

O primeiro trio de perguntas

Especifique quais caracteres precisam ser manipulados e como 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 invasor conseguisse inserir a string 'foo" onclick="evilCode()' em uma variável e a saída não fosse tratada, isso faria com que seu código fosse executado ao clicar no elemento:

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

Soluções para cada exemplo:

  1. os caracteres < e & representam o início de uma tag e de uma entidade HTML; substitua-os por &lt; e &amp;
  2. os caracteres " e & representam o fim de um valor de atributo e o início de uma entidade HTML; substitua-os por &quot; e &amp;
  3. os caracteres ' e & representam o fim de um valor de atributo e o início de uma entidade HTML; substitua-os por &apos; e &amp;

Você ganha um ponto para cada resposta correta. É claro que, em todos os três casos, você também pode substituir outros caracteres por entidades; isso não causa nenhum dano, mas não é necessário.

Pergunta nº 4

Continuando, quais caracteres precisam ser substituídos ao exibir uma variável nesse contexto?

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

Solução: Como você pode ver, as aspas estão faltando aqui. A maneira mais fácil é simplesmente adicionar as aspas e depois escapar como na pergunta anterior. Há também uma segunda solução, que é substituir os espaços e todos os caracteres que têm significado especial dentro de uma tag, como >, /, = e alguns outros por entidades HTML.

Pergunta nº 5

Agora está ficando mais interessante. Quais personagens precisam ser tratados nesse contexto:

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

Solução: Dentro da tag <script> as regras de escape são determinadas pelo JavaScript. As entidades HTML não são usadas aqui, mas há uma regra especial. Então, de quais caracteres escapamos? Dentro da cadeia de caracteres JavaScript, escapamos naturalmente do caractere ' que a delimita, usando uma barra invertida, substituindo-o por \'. Como o JavaScript não oferece suporte a cadeias de caracteres de várias linhas (exceto como literais de modelo), também precisamos escapar de caracteres de nova linha. Entretanto, lembre-se de que, além dos caracteres \n e \r, o JavaScript também considera os caracteres Unicode \u2028 e \u2029 como caracteres de nova linha, dos quais também devemos escapar. Por fim, a regra especial mencionada: a cadeia de caracteres não deve conter </script. Isso pode ser evitado, por exemplo, substituindo-o por <\/script.

Se você sabia disso, parabéns.

Pergunta nº 6

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

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

Solução: Novamente, as regras de escape para cadeias de caracteres JavaScript se aplicam aqui, mas, diferentemente do contexto anterior, em que as entidades HTML não foram escapadas, aqui elas são escapadas. Portanto, primeiro, escapamos a cadeia de caracteres JavaScript usando barras invertidas e, em seguida, substituímos os caracteres especiais (" e &) por entidades HTML. Tenha cuidado, pois a ordem correta é importante.

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

Pergunta nº 7

Vamos voltar do JavaScript para o HTML. Quais caracteres precisamos substituir dentro do comentário e como?

<!-- <?= $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 pode surpreendê-lo, é o par de caracteres --. O escape dessa sequência não é especificado, portanto, cabe a você decidir como substituí-la. Você pode intercalá-los com espaços. Ou, por exemplo, substituí-los por ==.

Pergunta nº 8

Estamos nos aproximando do final, portanto, vamos tentar variar a pergunta. Tente pensar sobre o que você precisa ter cuidado ao imprimir uma variável nesse contexto:

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

Solução: Além do escape, é importante verificar também se o URL não contém um esquema perigoso como javascript:, pois um URL composto dessa forma executaria o código do invasor quando clicado.

Pergunta nº 9

Finalmente, um presente para os verdadeiros conhecedores. Este é um exemplo de um aplicativo que usa uma estrutura JavaScript moderna, especificamente o Vue. Vamos ver se você consegue descobrir o que deve ser feito com cuidado ao imprimir uma 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>

Esse código cria um aplicativo Vue que será renderizado no elemento #app. O Vue interpreta o conteúdo desse elemento como seu modelo. E dentro do modelo, ele interpreta chaves duplas, que representam a saída de variáveis ou a chamada de código JavaScript (por exemplo, {{ foo }}).

Portanto, no elemento #app, além dos caracteres < e &, o par de {{ também tem um significado especial, que precisamos substituir por outra sequência apropriada para evitar que o Vue o interprete como sua própria tag. A substituição por entidades HTML não ajuda nesse caso. Como lidar com isso? Há um truque: insira um comentário HTML vazio entre as chaves {<!-- -->{e o Vue ignora essa sequência.

Resultados do questionário

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

No entanto, para garantir a segurança de seu site, é necessário tratar adequadamente a saída em todas as situações.

Se você ficou surpreso com a quantidade de contextos diferentes que podem aparecer em uma página HTML típica, saiba que ainda não mencionamos todos eles. Isso tornaria o questionário muito mais longo. No entanto, você não precisa ser um especialista em escape em todos os contextos se o seu sistema de modelos puder lidar com isso.

Então, vamos testá-los.

Qual é o desempenho dos sistemas de modelos?

Todos os sistemas de modelos modernos contam com um recurso de autoescaping que escapa automaticamente de todas as variáveis de saída. Se eles fizerem isso corretamente, seu site estará seguro. Se o fizerem de forma inadequada, o site estará exposto ao risco de vulnerabilidade XSS com todas as suas graves consequências.

Testaremos os sistemas de modelos populares a partir das perguntas deste questionário para determinar a eficácia de sua autodescapagem. Vamos começar a análise dos sistemas de modelos PHP.

Twig ❌

O primeiro é o sistema de modelos Twig (versão 3.5), mais comumente usado em conjunto com a estrutura Symfony. Vamos encarregá-lo de responder a todas as perguntas do questionário. A variável $str sempre será preenchida com uma string complicada, e veremos como ele lida com o resultado. Você pode ver os resultados à direita. Você também pode explorar suas respostas e seu 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 foi reprovado em seis dos nove testes!

Infelizmente, o escape automático do Twig funciona somente em texto e atributos HTML e, mesmo assim, somente quando eles estão entre aspas. Assim que as aspas estiverem faltando, o Twig não informa nenhum erro e cria uma falha de segurança XSS.

Isso é particularmente desagradável porque é assim que os valores de atributos são escritos em bibliotecas populares como React ou Svelte. Um programador que usa tanto o Twig quanto o React pode naturalmente esquecer as aspas.

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

Blade ❌❌

O segundo participante é o sistema de modelos Blade (versão 10.9), que é fortemente integrado ao Laravel e seu ecossistema. Novamente, testaremos suas habilidades em nossas perguntas do questionário. 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>

A lâmina falhou em seis de nove testes!

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

No entanto, o que é surpreendente é a falha da diretiva @php no Blade, que causa a saída de seu próprio código PHP diretamente para a saída, como visto na última linha.

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>

Latte se destacou em todas as nove tarefas!

Ele conseguiu lidar com aspas ausentes em atributos HTML, processou JavaScript tanto no elemento <script> e em atributos, e lidou com a sequência proibida em comentários HTML.

Além disso, evitou uma situação em que clicar em um link malicioso fornecido por um invasor poderia executar seu código. E conseguiu lidar com o escape de tags para o Vue.

Teste bônus

Um dos recursos essenciais de todos os sistemas de modelos é trabalhar com blocos e a herança de modelos relacionados. Portanto, daremos a todos os sistemas de modelos testados mais uma tarefa. Criaremos um bloco description, que será impresso em um atributo HTML. No mundo real, a definição do bloco estaria, obviamente, localizada no modelo filho e sua saída no modelo pai, como o layout. Essa é apenas uma forma simplificada, mas é suficiente para testar o autoescaping ao gerar blocos. Qual foi o desempenho?

Twig: failed ❌ ao gerar blocos, os caracteres não são escapados corretamente

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

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




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

Blade: falha ❌ na saída de blocos, os caracteres não são escapados corretamente

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

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




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

Latte: passou ✅ ao gerar 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 estão vulneráveis?

O autoescapamento em sistemas como Twig, Blade ou Smarty funciona simplesmente substituindo cinco caracteres <>"'& por entidades HTML e não faz distinção de contexto. Portanto, ele só funciona em algumas situações e falha em todas as outras. O autoescaping ingênuo é um recurso perigoso porque cria uma falsa sensação de segurança.

Não é de surpreender, portanto, que atualmente mais de 27% dos sites tenham vulnerabilidades críticas, principalmente XSS (fonte: Acunetix Web Vulnerability Report). Como sair dessa situação? Use um sistema de modelos que diferencie os contextos.

O Latte é o único sistema de modelos PHP que não percebe um modelo como apenas uma sequência de caracteres, mas entende HTML. Ele sabe o que são tags, atributos, etc. Ele distingue contextos. E, portanto, ele faz corretamente o escape no texto HTML, de forma diferente dentro das tags HTML, de forma diferente dentro do JavaScript, etc.

O Latte representa, portanto, o único sistema de modelos 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>