O objetivo da nova API Sanitizer é criar um processador robusto para que strings arbitrárias sejam inseridas com segurança em uma página.
Os aplicativos lidam com strings não confiáveis o tempo todo, mas renderizar esse conteúdo com segurança como parte de um documento HTML pode ser complicado. Sem o cuidado necessário, é fácil criar acidentalmente oportunidades de scripting em vários locais (XSS) que invasores maliciosos podem explorar.
Para reduzir esse risco, a nova proposta da API Sanitizer tem como objetivo 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 seu uso.
// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())
Como escapar entradas do usuário
Ao inserir a entrada do usuário, as strings de consulta, o conteúdo do cookie e assim por diante, no DOM, as strings devem ter códigos de escape adequados. É preciso dar atenção especial à manipulação do DOM via .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 o <em>
adicionado pelo usuário também é expandido como uma string, esse método não pode ser usado para manter a decoração do texto em HTML.
A melhor coisa a se fazer aqui não é examinar, mas limpar.
Como limpar a entrada do usuário
A diferença entre escape e limpeza
O escape refere-se à substituição de caracteres HTML especiais por entidades HTML.
A limpeza refere-se à remoção de partes semanticamente prejudiciais (como a execução de script) de strings HTML.
Exemplo
No exemplo anterior, <img onerror>
faz com que o gerenciador de erros seja executado, mas se o gerenciador onerror
for removido, seria possível expandi-lo com segurança no DOM, deixando o <em>
intacto.
// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`
Para realizar a limpeza correta, é necessário analisar a string de entrada como HTML, omitir tags e atributos 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, { sanitizer: new Sanitizer() }
é o argumento padrão. Pode ser como mostrado abaixo.
$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>
É importante 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 uma vez internamente e o resultado é expandido diretamente para o DOM.
Para acessar 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 precisa tratar o elemento especificado.
allowElements
: nomes de elementos que o limpador precisa reter.
blockElements
: nomes de elementos que o limpador precisa remover e manter os filhos.
dropElements
: nomes dos elementos que o limpador precisa remover e 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 ferramenta de limpeza vai permitir ou negar atributos especificados com as seguintes opções:
allowAttributes
dropAttributes
As propriedades allowAttributes
e dropAttributes
esperam listas de correspondência de atributo, 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 de permitir ou negar elementos personalizados. Se forem permitidos, outras configurações para 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
A DOMPurify é uma biblioteca bem conhecida que oferece a funcionalidade de limpeza. 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 do DOMPurify tem algumas desvantagens. Se uma string for retornada, a string de entrada será analisada duas vezes: 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ó aceita uma string como argumento, o contexto de análise precisa ser adivinhado.
A API Sanitizer melhora a abordagem DOMPurify e foi criada para eliminar a necessidade de análise dupla e esclarecer o contexto da análise.
Status da API e suporte ao navegador
A API Sanitizer está em discussão no processo de padronização, e o Chrome está em processo de implementação.
Step | Status |
---|---|
1. Criar explicação | Concluído |
2. Criar rascunho de especificação | Concluído |
3. Reunir feedbacks e iterar no design | Concluído |
4. Teste de origem do Chrome | Concluído |
5. lançamento | Intent de envio na versão M105 |
Mozilla: considera que essa proposta vale a prototipagem e está implementando ativamente (links em inglês).
WebKit: veja a resposta na lista de e-mails do WebKit.
Como ativar a API Sanitizer
Como ativar por about://flags
ou opção de CLI
Chrome
O Chrome está em processo de implementação da API Sanitizer. No Chrome 93 ou mais recente, é possível testar o comportamento ativando a sinalização about://flags/#enable-experimental-web-platform-features
. Nas versões anteriores do Chrome Canary e do Canal de Desenvolvedor, você pode ativar esse recurso via --enable-blink-features=SanitizerAPI
e testar 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á-la, defina a sinalização 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 da API Sanitizer no GitHub (em inglês) e discuta com os autores das especificações e as pessoas interessadas nessa API.
Se você encontrar algum bug ou comportamento inesperado na implementação do Chrome, registre um bug para informá-lo. Selecione os componentes de 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
- Especificação da API HTML Sanitizer
- Repositório WICG/sanitizer-api (em inglês)
- Perguntas frequentes sobre a API Sanitizer
- Documentação de referência da API HTML Sanitizer no MDN
Foto de Towfiqu barbhuiya no Unsplash.