Questionário: Você consegue se defender contra a vulnerabilidade XSS?
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 <
, 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" onclick="evilCode()">
Soluções para cada exemplo:
- os caracteres
<
e&
representam o início de uma tag e de uma entidade HTML; substitua-os por<
e&
- os caracteres
"
e&
representam o fim de um valor de atributo e o início de uma entidade HTML; substitua-os por"
e&
- os caracteres
'
e&
representam o fim de um valor de atributo e o início de uma entidade HTML; substitua-os por'
e&
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><'"&</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 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><'"&</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>
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><'"&</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>
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' 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>
Para enviar um comentário, faça o login