Exibir HTML gerado pelo usuário com segurança é um desafio comum. A API Sanitizer nativa oferece uma maneira integrada e otimizada para navegadores de remover conteúdo perigoso (como tags <script> ou atributos on*) antes que ele chegue ao DOM.
A limpeza do lado do cliente protege a página que renderiza o conteúdo. Se o mesmo conteúdo gerado pelo usuário também for renderizado em outro lugar (outros clientes, páginas renderizadas pelo servidor ou e-mails), limpe-o no servidor, além de usar a defesa em profundidade, e verifique se as regras de limpeza do servidor e do cliente não entram em conflito.
1. Como inserir HTML com segurança usando setHTML()
A principal maneira de usar a API Sanitizer é pelo método setHTML() em qualquer Element. Esse método lida com a análise, a limpeza e a inserção em uma única etapa, garantindo que o conteúdo seja limpo especificamente para o contexto em que será inserido.
const untrustedHTML = `
<p>Hello!</p>
<script>alert('XSS')</script>
<img src="x" onerror="alert('XSS')">
<div onclick="doBadThing()">Click me</div>
`;
const container = document.getElementById('output');
// Sanitize and insert in one call
// The default sanitizer removes scripts and event handlers, plus other
// elements and attributes outside its built-in allowlist (such as style)
container.setHTML(untrustedHTML);
2. Como usar configurações personalizadas de limpador
É possível personalizar as regras de limpeza criando uma instância Sanitizer. Isso permite definir exatamente quais elementos e atributos são permitidos para seu caso de uso específico.
Ao usar Element.setHTML() e Document.parseHTML(), as configurações padrão de Sanitizer removem todos os elementos e atributos não seguros para XSS. Não é possível criar um Sanitizer mais permissivo que permita esses elementos, mas você pode restringir ainda mais o que é permitido.
// Define a restrictive configuration that only allows the specified elements and attributes.
const config = {
elements: ["p", "b", "i", "strong", "em"],
attributes: ["class"],
replaceWithChildrenElements: ['div']
};
const mySanitizer = new Sanitizer(config);
// Apply custom sanitization
container.setHTML(untrustedHTML, { sanitizer: mySanitizer });
Os elementos que não estão em elements são removidos junto com o conteúdo. Liste os elementos de wrapper com o texto que você quer manter (como div acima) em replaceWithChildrenElements, que remove o elemento, mas mantém os filhos.
Se você precisar de um Sanitizer mais permissivo que permita alguns elementos ou atributos não seguros para XSS, use um Sanitizer personalizado com Element.setHTMLUnsafe() e Document.parseHTMLUnsafe().
3. Analisar a entrada do usuário com Document.parseHTML()
Se você precisar analisar HTML não confiável sem inseri-lo imediatamente no DOM ativo, use Document.parseHTML(). Esse método retorna um Document que foi limpo de acordo com regras padrão ou personalizadas. Assim, é possível inspecionar ou transformar os nós limpos (por exemplo, para criar uma prévia ou extrair texto) antes de inseri-los.
Mova os nós resultantes para a página em vez de serializá-los em uma string e analisá-los novamente com innerHTML, o que pode reintroduzir o XSS de mutação (mXSS).
const rawHTML = '<p>Hello <script>console.log("bad")</script></p>';
const doc = Document.parseHTML(rawHTML);
// doc.body now contains the sanitized nodes: <p>Hello </p>
preview.replaceChildren(...doc.body.childNodes);
Alternativas e suporte do navegador para a API Sanitizer
Se a API Sanitizer nativa não estiver disponível nos navegadores de destino, carregue uma biblioteca como o DOMPurify somente quando o suporte nativo estiver ausente, para evitar o envio dela para navegadores que não precisam dela. Use as verificações in para detectar recursos, de modo que a verificação em si funcione em navegadores mais antigos.
let domPurifyPromise;
function loadDOMPurify() {
// Code-split with your bundler so DOMPurify is only downloaded when needed
domPurifyPromise ??= import('dompurify').then((mod) => mod.default ?? mod);
return domPurifyPromise;
}
export async function safeSetHTML(el, html) {
const elements = ['p', 'b', 'i', 'strong', 'em'];
const attributes = ['class'];
if ('setHTML' in Element.prototype) {
// setHTML() accepts a plain configuration object as the sanitizer
el.setHTML(html, { sanitizer: { elements, attributes } });
} else {
const DOMPurify = await loadDOMPurify();
el.innerHTML = DOMPurify.sanitize(html, {
ALLOWED_TAGS: [...elements, '#text'],
ALLOWED_ATTR: attributes,
// Match the native Sanitizer: strip aria-*/data-* and disallowed element content
ALLOW_ARIA_ATTR: false,
ALLOW_DATA_ATTR: false,
KEEP_CONTENT: false
});
}
}
Por padrão, o DOMPurify mantém os atributos aria-* e data-* (mesmo quando ALLOWED_ATTR é especificado), bem como o conteúdo de texto dos elementos que ele remove, enquanto o Sanitizer nativo remove os atributos aria-* e data-* e os elementos não permitidos junto com o conteúdo deles. Definir ALLOW_ARIA_ATTR: false, ALLOW_DATA_ATTR: false, KEEP_CONTENT: false e permitir #text faz com que o DOMPurify corresponda ao comportamento nativo. O DOMPurify não tem uma opção integrada equivalente a replaceWithChildrenElements. Portanto, evite essa opção em configurações que precisam de um substituto correspondente.
Se a página aplicar Tipos confiáveis (require-trusted-types-for 'script'), a atribuição de uma string a innerHTML vai gerar um erro. setHTML() não precisa de uma política de Tipos Confiáveis, mas o substituto precisa: transmita RETURN_TRUSTED_TYPE: true para DOMPurify.sanitize() e permita o nome da política dompurify na diretiva CSP trusted-types.