Limpeza de HTML não confiável

Recursos da Web: API Sanitizer

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.