Sobre o mecanismo React para evitar a possibilidade de injeção de JSON para XSS e sobre como evitar vulnerabilidades comuns.
Você pode pensar que está escrevendo JSX:
<marquee bgcolor="#ffa7c4">hi</marquee>
Mas, na verdade, você está chamando a função:
React.createElement( 'marquee', { bgcolor: '#ffa7c4' }, 'hi' )
E essa função retorna um objeto regular chamado elemento React. Assim, após atravessar todos os componentes, é obtida uma árvore de objetos semelhantes:
{ type: 'marquee', props: { bgcolor: '#ffa7c4', children: 'hi', }, key: null, ref: null, $$typeof: Symbol.for('react.element'), }
Se você usou o React antes, pode estar familiarizado com os campos type, props, key e ref. Mas qual é a propriedade $$typeof
? E por que tem o símbolo Symbol()
como seu valor?
Antes que as bibliotecas da interface do usuário se tornassem populares, para exibir a entrada do cliente no código do aplicativo, uma linha contendo marcação HTML foi gerada e inserida diretamente no DOM, via innerHTML:
const messageEl = document.getElementById('message'); messageEl.innerHTML = '<p>' + message.text + '</p>';
Esse mecanismo funciona bem, a menos que message.text
esteja definido como <img src onerror="stealYourPassword()">
. Portanto, concluímos que você não precisa interpretar todas as entradas do cliente como marcação HTML.
Para se proteger contra esses ataques, você pode usar APIs seguras, como document.createTextNode()
ou textContent
, que não interpretam o texto. E, como medida extra, escape as strings substituindo caracteres potencialmente perigosos como <
, >
por caracteres seguros.
No entanto, a probabilidade de erro é alta, pois é difícil rastrear todos os locais onde você usa as informações registradas pelo usuário em sua página. É por isso que bibliotecas modernas, como o React, trabalham com segurança com qualquer texto padrão:
<p> {message.text} </p>
Se message.text
for uma string maliciosa com uma <img>
, ela não se transformará em uma <img>
real. React escapa o conteúdo do texto e o adiciona ao DOM. Portanto, em vez de ver a <img>
, você simplesmente vê sua marcação como uma sequência.
Para exibir HTML arbitrário dentro de um elemento React, você deve usar a seguinte construção: dangerouslySetInnerHTML={{ __html: message.text }}
. O design é intencionalmente desconfortável. Devido ao seu absurdo, ele se torna mais visível e atrai a atenção ao visualizar o código.
Isso significa que o React é completamente seguro? Não. Existem muitos métodos de ataque conhecidos baseados em HTML e DOM. Os atributos de tag merecem atenção especial. Por exemplo, se você escrever <a href={user.website}>
, poderá substituir o código malicioso: 'javascript: stealYourPassword()'
como um link de texto.
Na maioria dos casos, a presença de vulnerabilidades no lado do cliente é o resultado de problemas no lado do servidor e deve ser corrigida antes de tudo.
No entanto, a exibição segura do conteúdo de texto personalizado é uma primeira linha de defesa razoável que reflete muitos ataques em potencial.
Com base em considerações anteriores, podemos concluir que o seguinte código deve ser completamente seguro:
Mas esse também não é o caso. E aqui chegamos mais perto de explicar a presença de $$typeof
no elemento React.
Como explicamos anteriormente, os elementos React são objetos simples:
{ type: 'marquee', props: { bgcolor: '#ffa7c4', children: 'hi', }, key: null, ref: null, $$typeof: Symbol.for('react.element'), }
Normalmente, um elemento React é criado chamando a função React.createElement()
, mas você pode criá-lo imediatamente com um literal, como acabei de fazer acima.
Suponha que armazenemos no servidor uma sequência que o usuário nos enviou anteriormente e cada vez que a exibimos no lado do cliente. Mas alguém, em vez de uma string, nos enviou JSON:
let expectedTextButGotJSON = { type: 'div', props: { dangerouslySetInnerHTML: { __html: '/* */' }, },
Ou seja, de repente, em vez da sequência esperada, o valor da variável expectTextButGotJSON acabou sendo JSON. Que será processado pelo React como um literal e, portanto, executará um código malicioso.
O React 0.13 é vulnerável a um ataque semelhante ao XSS, mas a partir da versão 0.14, cada elemento é marcado com um símbolo:
{ type: 'marquee', props: { bgcolor: '#ffa7c4', children: 'hi', }, key: null, ref: null, $$typeof: Symbol.for('react.element'), }
Essa proteção funciona porque os caracteres não são um valor JSON válido. Portanto, mesmo se o servidor tiver uma vulnerabilidade em potencial e retornar JSON em vez de texto, o JSON não poderá conter Symbol.for('response.element')
. React verifica o elemento quanto ao element.$$typeof
e se recusa a processar o elemento se ele estiver ausente ou inválido.
A principal vantagem do Symbol.for()
é que os símbolos são globais entre os contextos porque eles usam um registro global. Isso garante o mesmo valor de retorno, mesmo em um iframe. E mesmo se houver várias cópias do React na página, elas ainda poderão "corresponder" por meio de um único valor de $$typeof
.
E os navegadores que não suportam caracteres?
Infelizmente, eles não poderão implementar a proteção adicional discutida acima, mas os elementos React ainda conterão a propriedade $$typeof
por consistência, mas será apenas um número - 0xeac7
.
Por que exatamente 0xeac7
? Porque parece reagir.