Manipulação segura do DOM com a API Sanitizer

O objetivo da nova API Sanitizer é criar um processador robusto para que strings arbitrárias sejam inseridas com segurança em uma página.

Jack J
Jack J

Os aplicativos lidam com strings não confiáveis o tempo todo, mas renderizar com segurança esse conteúdo como parte de um documento HTML pode ser complicado. Sem cuidado suficiente, é fácil criar acidentalmente oportunidades para scripting em vários sites (XSS) que invasores maliciosos podem explorar.

Para reduzir esse risco, o objetivo da nova proposta da API Sanitizer é criar um processador robusto para que strings arbitrárias sejam inseridas com segurança em uma página. Este artigo apresenta a API e explica o uso dela.

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())

escapar entrada do usuário

Ao inserir entradas do usuário, strings de consulta, conteúdos de cookies e assim por diante, no DOM, é necessário fazer o escape adequado das strings. É preciso prestar atenção especial à manipulação do DOM pelo .innerHTML, em que strings sem escape são uma fonte típica de XSS.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.innerHTML = user_input

Se você fizer o escape de caracteres especiais HTML na string de entrada acima ou expandi-la usando .textContent, alert(0) não será executado. No entanto, como a <em> adicionada pelo usuário também é expandida como uma string, esse método não pode ser usado para manter a decoração do texto em HTML.

A melhor coisa a fazer aqui é não escapar, mas sanitizar.

Como limpar a entrada do usuário

A diferença entre escapar e limpar

O escape refere-se à substituição de caracteres HTML especiais por Entidades HTML.

A limpeza refere-se à remoção de partes semanticamente prejudiciais (como execução de script) de strings HTML.

Exemplo

No exemplo anterior, <img onerror> faz com que o gerenciador de erros seja executado. No entanto, se o gerenciador onerror fosse removido, seria possível expandi-lo com segurança no DOM e deixar <em> intacto.

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

Para fazer uma limpeza correta, é necessário analisar a string de entrada como HTML, omitir tags e atributos que são considerados prejudiciais e manter os inofensivos.

A especificação da API Sanitizer proposta tem como objetivo fornecer esse processamento como uma API padrão para navegadores.

API Sanitizer

A API Sanitizer é usada da seguinte maneira:

const $div = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.setHTML(user_input, { sanitizer: new Sanitizer() }) // <div><em>hello world</em><img src=""></div>

No entanto, o { sanitizer: new Sanitizer() } é o argumento padrão. Ele pode ficar assim.

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

Vale ressaltar que setHTML() é definido em Element. Por ser um método de Element, o contexto a ser analisado é autoexplicativo (<div>, neste caso). A análise é feita internamente uma vez e o resultado é diretamente expandido para o DOM.

Para receber o resultado da limpeza como uma string, use .innerHTML dos resultados de setHTML().

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

Personalizar pela configuração

A API Sanitizer é configurada por padrão para remover strings que acionariam a execução do script. No entanto, também é possível adicionar suas próprias personalizações ao processo de limpeza usando um objeto de configuração.

const config = {
  allowElements: [],
  blockElements: [],
  dropElements: [],
  allowAttributes: {},
  dropAttributes: {},
  allowCustomElements: true,
  allowComments: true
};
// sanitized result is customized by configuration
new Sanitizer(config)

As opções a seguir especificam como o resultado da limpeza deve tratar o elemento especificado.

allowElements: nomes dos elementos que a limpeza precisa reter.

blockElements: nomes de elementos que a limpeza precisa remover, mantendo os filhos.

dropElements: nomes dos elementos que a limpeza precisa remover, assim como os filhos deles.

const str = `hello <b><i>world</i></b>`

$div.setHTML(str)
// <div>hello <b><i>world</i></b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: [ "b" ]}) })
// <div>hello <b>world</b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ "b" ]}) })
// <div>hello <i>world</i></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: []}) })
// <div>hello world</div>

Também é possível controlar se a limpeza vai permitir ou negar atributos especificados com as seguintes opções:

  • allowAttributes
  • dropAttributes

As propriedades allowAttributes e dropAttributes esperam listas de correspondências de atributo, ou seja, objetos com chaves que são nomes de atributos, e valores são listas de elementos de destino ou o caractere curinga *.

const str = `<span id=foo class=bar style="color: red">hello</span>`

$div.setHTML(str)
// <div><span id="foo" class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["span"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["p"]}}) })
// <div><span>hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["*"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({dropAttributes: {"id": ["span"]}}) })
// <div><span class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// <div>hello</div>

allowCustomElements é a opção para permitir ou negar elementos personalizados. Se forem permitidas, outras configurações de elementos e atributos ainda se aplicam.

const str = `<custom-elem>hello</custom-elem>`

$div.setHTML(str)
// <div></div>

const sanitizer = new Sanitizer({
  allowCustomElements: true,
  allowElements: ["div", "custom-elem"]
})
$div.setHTML(str, { sanitizer })
// <div><custom-elem>hello</custom-elem></div>

Superfície da API

Comparação com o DomPurify

DOMPurify é uma biblioteca conhecida que oferece funcionalidades de sanitização. A principal diferença entre a API Sanitizer e o DOMPurify é que o DOMPurify retorna o resultado da limpeza como uma string, que você precisa gravar em um elemento DOM usando .innerHTML.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `<em>hello world</em><img src="">`

O DOMPurify pode servir como substituto quando a API Sanitizer não está implementada no navegador.

A implementação de DOMPurify tem algumas desvantagens. Se uma string for retornada, a string de entrada será analisada duas vezes, por DOMPurify e .innerHTML. Essa análise dupla desperdiça tempo de processamento, mas também pode levar a vulnerabilidades interessantes causadas por casos em que o resultado da segunda análise é diferente da primeira.

O HTML também precisa de contexto para ser analisado. Por exemplo, <td> faz sentido em <table>, mas não em <div>. Como DOMPurify.sanitize() só usa uma string como argumento, era preciso adivinhar o contexto de análise.

A API Sanitizer melhora a abordagem DOMPurify e foi projetada para eliminar a necessidade de análise dupla e esclarecer o contexto da análise.

Status da API e compatibilidade com navegadores

A API Sanitizer está em discussão no processo de padronização, e o Chrome está no processo de implementação.

Etapa Status
1. Criar explicação Concluído
2. Criar rascunho de especificação Concluído
3. Reunir feedback e iterar o design Concluído
4. Teste de origem do Chrome Concluído
5. Lançamento Intenção de enviar no M105

Mozilla: considera que esta proposta vale a pena fazer um protótipo e está implementando-a ativamente.

WebKit: veja a resposta na lista de e-mails do WebKit.

Como ativar a API Sanitizer

Compatibilidade com navegadores

  • Chrome: incompatível.
  • Edge: não compatível.
  • Firefox: atrás de uma sinalização.
  • Safari: incompatível.

Origem

Ativação via about://flags ou opção da CLI

Chrome

O Chrome está no processo de implementação da API Sanitizer. No Chrome 93 ou mais recente, ative a sinalização about://flags/#enable-experimental-web-platform-features para testar o comportamento. Nas versões anteriores do Chrome Canary e do Canal de Desenvolvedor, você pode ativá-lo via --enable-blink-features=SanitizerAPI e testá-lo agora mesmo. Confira as instruções sobre como executar o Chrome com sinalizações.

Firefox

O Firefox também implementa a API Sanitizer como um recurso experimental. Para ativá-lo, defina a flag dom.security.sanitizer.enabled como true em about:config.

Detecção de recursos

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

Feedback

Se você testar essa API e tiver algum feedback, adoraríamos saber. Compartilhe sua opinião sobre os problemas do GitHub da API Sanitizer e converse com os autores das especificações e as pessoas interessadas nessa API.

Se você encontrar bugs ou comportamento inesperado na implementação do Chrome, registre um bug para informar. Selecione os componentes Blink>SecurityFeature>SanitizerAPI e compartilhe detalhes para ajudar os implementadores a acompanhar o problema.

Demonstração

Para conferir a API Sanitizer em ação, confira o Playground da API Sanitizer, de Mike West:

Referências


Foto de Towfiqu barbhuiya no Unsplash.