Quiz: você consegue se defender da vulnerabilidade XSS?
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 <
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" onclick="evilCode()">
Solução dos exemplos individuais:
- os caracteres
<
e&
representam o início de uma tag HTML e entidades, substituímo-los por<
e&
- os caracteres
"
e&
representam o fim do valor do atributo e o início de uma entidade HTML, substituímo-los por"
e&
- os caracteres
'
e&
representam o fim do valor do atributo e o início de uma entidade HTML, substituímo-los por'
e&
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><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"u{2028}; </script>
❌ <p onclick="foo('"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><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '" ; </script>
❌ <p onclick="foo('" )"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌❌ <div id="app"> <?php echo e(foo); ?> </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><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"\u2028; </script>
❌ <p onclick="foo('"\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><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
✅ <input value="foo onclick=evilCode()">
✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo("'\"\n\u2028")"></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' 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>
Faça o login para enviar um comentário