A nova API Sanitizer tem como objetivo 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 cuidado suficiente, é fácil criar acidentalmente oportunidades para scripting em vários sites (XSS) que invasores maliciosos podem explorar.
Para reduzir esse risco, a nova proposta da API Sanitizer (link em inglês) visa 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 como usá-la.
// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerro>r=alert(0)`, new Sanitizer())
Como escapar da entrada do usuário
Ao inserir entradas do usuário, strings de consulta, conteúdo de cookies etc. no DOM, as strings precisam ser escapadas corretamente. É preciso prestar 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="" onerro>r=alert(0)`
$div.innerHTML = user_input
Se você fizer o escape dos caracteres especiais de HTML na string de entrada acima ou expandi-la usando .textContent, alert(0) não será executado. No entanto, como <em> adicionado pelo usuário também é expandido como uma string, esse método não pode ser usado para manter a decoração de texto em HTML.
A melhor coisa a fazer aqui não é escapar, mas higienizar.
Sanitização de entrada do usuário
A diferença entre escapar e higienizar
O escape se refere à substituição de caracteres HTML especiais por entidades HTML.
A sanitização se refere à 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, mas se o gerenciador onerror fosse removido, seria possível expandi-lo com segurança no DOM, deixando <em> intacto.
// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o world/em>img src=""`
Para fazer a sanitização corretamente, é necessário analisar a string de entrada como HTML, omitir tags e atributos considerados prejudiciais e manter os inofensivos.
A especificação proposta da API Sanitizer (link em inglês) 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_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg src=""/div
No entanto, { sanitizer: new Sanitizer() } é o argumento padrão. Então, pode ser como abaixo.
$div.setHTML(user_input) // <div><em>hello world</em><img src=&q><uot;>"/div
Vale a pena observar que setHTML() é definido em Element. Como é 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 no DOM.
Para receber o resultado da limpeza como uma string, use .innerHTML dos resultados setHTML().
const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g src=""
Personalizar por configuração
A API Sanitizer é configurada por padrão para remover strings que acionariam a execução de scripts. No entanto, também é possível adicionar suas próprias personalizações ao processo de higienização 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 higienização deve tratar o elemento especificado.
allowElements: nomes de elementos que o higienizador precisa reter.
blockElements: nomes dos elementos que o higienizador precisa remover, mantendo os filhos.
dropElements: nomes dos elementos que o higienizador precisa remover, junto com 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" <]})> })
//< >divhe<ll><o bw>orld/b/div
$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div
Você também pode controlar se o higienizador vai permitir ou negar atributos especificados com as seguintes opções:
allowAttributesdropAttributes
As propriedades allowAttributes e dropAttributes esperam listas de correspondência de atributos, ou seja, objetos cujas chaves são nomes de atributos e cujos valores são listas de elementos de destino ou o caractere curinga *.
const str = `<span id=foo class=bar style="color:> red&<quot;>hello/span`
$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div
$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<
$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div
$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// divhello/div
allowCustomElements é a opção para permitir ou negar elementos personalizados. Se eles forem permitidos, outras configurações para elementos e atributos ainda serão aplicadas.
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 >})
//< divcustom-e><lemh>ello/custom-elem/div
Superfície da API
Comparação com o DomPurify
DOMPurify é uma biblioteca conhecida que oferece funcionalidade de limpeza. A principal diferença entre a API Sanitizer e o DOMPurify é que o DOMPurify retorna o resultado da sanitização como uma string, que você precisa gravar em um elemento DOM usando .innerHTML.
const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o world/em>img src=""`
O DOMPurify pode servir como substituto quando a API Sanitizer não é implementada no navegador.
A implementação do DOMPurify tem algumas desvantagens. Se uma string for retornada, a string de entrada será analisada duas vezes, pelo DOMPurify e pelo .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 precisou ser adivinhado.
A API Sanitizer melhora a abordagem do DOMPurify e foi projetada para eliminar a necessidade de análise dupla e esclarecer o contexto de 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á em processo de implementação.
| Etapa | Status |
|---|---|
| 1. Criar explicação | Concluído |
| 2. Criar rascunho de especificação | Concluído |
| 3. Coletar feedback e iterar o design | Concluído |
| 4. Teste de origem do Chrome | Concluído |
| 5. Lançamento | Intenção de envio no M105 |
Mozilla: considera que essa proposta vale a pena ser prototipada e está implementando-a ativamente.
WebKit: confira a resposta na lista de e-mails do WebKit (em inglês).
Como ativar a API Sanitizer
Browser Support
Ativação usando a opção about://flags ou CLI
Chrome
O Chrome está implementando a API Sanitizer. No Chrome 93 ou em versões mais recentes, você pode testar o comportamento ativando a flag about://flags/#enable-experimental-web-platform-features. Em versões anteriores do Chrome Canary e do Canal de desenvolvimento, é possível ativar o recurso em --enable-blink-features=SanitizerAPI e testar agora mesmo. Confira as instruções de como executar o Chrome com flags.
Firefox
O Firefox também implementa a API Sanitizer como um recurso experimental. Para ativar, 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, envie para nós. Compartilhe suas ideias sobre os problemas do GitHub da API Sanitizer e discuta com os autores da especificação e pessoas interessadas nessa API.
Se você encontrar bugs ou comportamentos inesperados na implementação do Chrome, registre um bug para informar o problema. Selecione os componentes Blink>SecurityFeature>SanitizerAPI e compartilhe detalhes para ajudar os implementadores a rastrear o problema.
Demonstração
Para conferir a API Sanitizer em ação, acesse o Sanitizer API Playground (link em inglês) de Mike West:
Referências
- Especificação da API HTML Sanitizer
- Repositório WICG/sanitizer-api
- Perguntas frequentes sobre a API Sanitizer
- Documentação de referência da API HTML Sanitizer na MDN
Foto de Towfiqu barbhuiya no Unsplash.