Um site criado por IA pode personalizar um título com a query string, renderizar uma resposta de API ou montar uma confirmação de formulário usando innerHTML. O resultado parece correto com dados comuns. Quando uma entrada controlável alcança uma API que interpreta HTML ou JavaScript, porém, a mesma conveniência pode virar DOM XSS.
Prevenir DOM XSS é rastrear fontes de dados até os pontos em que o navegador os interpreta. A correção principal acontece no código: mantenha texto como texto, construa elementos com APIs seguras, valide URLs e reserve parsing de HTML para casos realmente necessários e sanitizados.
Este guia organiza a revisão no código, na build e na URL publicada sem tratar Content Security Policy como substituto para remover o fluxo vulnerável.
Entenda o fluxo: fonte, transformação e sink
DOM XSS nasce no navegador. Um valor chega de uma fonte controlável, atravessa transformações e alcança um sink capaz de interpretar marcação, script ou URL executável.
Fontes comuns incluem:
location.search,location.hashe partes da URL;- campos de formulário;
- respostas de API;
localStorage,sessionStoragee IndexedDB;postMessage;- atributos
data-*e conteúdo inserido por terceiros; - configuração remota, CMS e arquivos JSON.
Sinks de alto risco incluem:
innerHTML,outerHTMLeinsertAdjacentHTML();document.write();eval()eFunction();- timers que recebem código como string;
- criação de
<script>com origem controlável; - navegação para URLs não validadas, especialmente esquemas executáveis.
A especificação de Trusted Types do W3C chama essas APIs poderosas de injection sinks. A presença de um sink não prova vulnerabilidade sozinha; o risco aparece quando uma entrada não confiável pode alcançá-lo sem uma fronteira adequada.
Faça uma busca orientada a dados
Comece pelas fontes e sinks, sem editar:
rg -n \ 'innerHTML|outerHTML|insertAdjacentHTML|document\.write|eval\(|new Function|setTimeout\(|setInterval\(' \ src public
rg -n \ 'location\.(search|hash|href)|URLSearchParams|postMessage|localStorage|sessionStorage|fetch\(' \ src publicExpanda os caminhos conforme o projeto. Depois descreva cada fluxo:
fonte:quem controla o valor:transformações:sink:contexto interpretado (texto, HTML, atributo, URL ou script):validação ou sanitização existente:teste que alcança o fluxo:Não encerre a revisão porque o valor veio da própria API ou do armazenamento local. APIs podem refletir conteúdo externo, e estado do navegador pode ser alterado por uma pessoa, extensão, script de terceiro ou versão anterior da aplicação.
Use texto como texto
Quando o objetivo é exibir uma string, use textContent. A documentação da MDN recomenda evitar innerHTML para texto porque ele invoca o parser de HTML e pode abrir risco de XSS.
Evite:
const params = new URLSearchParams(location.search);message.innerHTML = `Olá, ${params.get("name")}`;Prefira:
const params = new URLSearchParams(location.search);const name = params.get("name") ?? "visitante";
message.textContent = `Olá, ${name}`;O segundo exemplo não transforma a string em marcação. Ainda pode ser necessário limitar tamanho, normalizar espaços ou validar o conteúdo conforme a experiência, mas caracteres de HTML permanecem texto.
Para estruturas compostas, crie nós explicitamente:
const strong = document.createElement("strong");strong.textContent = name;
message.replaceChildren("Olá, ", strong);Essa abordagem também deixa visível qual parte é estrutura criada pelo código e qual parte é conteúdo recebido.
Valide URLs pelo destino e pelo protocolo
Nem todo problema passa por innerHTML. Um valor pode controlar href, src, action ou uma navegação por JavaScript.
Analise a URL com a API URL, defina a base esperada e aplique uma allowlist:
const candidate = new URL(rawUrl, location.origin);const allowedProtocols = new Set(["https:", "http:"]);
if (!allowedProtocols.has(candidate.protocol)) { throw new Error("Unsupported URL protocol");}Para links que deveriam permanecer no próprio site, exija também candidate.origin === location.origin. Para um endpoint externo, compare origem e caminho com o contrato da integração. Não aceite uma URL só porque contém um domínio esperado como substring.
Evite montar HTML de link com interpolação. Crie o elemento, atribua uma URL validada e use textContent para o rótulo.
Se HTML for requisito, trate-o como uma fronteira excepcional
Alguns projetos realmente recebem conteúdo rico. Nesse caso, escapar tudo muda o produto, mas enviar a string diretamente para innerHTML também não é aceitável.
Antes de permitir HTML:
- confirme por que texto ou componentes conhecidos não resolvem;
- defina quais elementos e atributos são necessários;
- use um sanitizador de HTML mantido e configurado por allowlist;
- centralize a política em um único módulo revisável;
- teste payloads de atributos, URLs e estruturas aninhadas;
- preserve a sanitização na última fronteira antes do sink;
- registre a dependência e seu processo de atualização.
A documentação de innerHTML da MDN classifica a propriedade como injection sink e recomenda TrustedHTML com enforcement de Trusted Types para reduzir o risco. Trusted Types não sanitiza por conta própria: a política ainda precisa transformar ou rejeitar a entrada corretamente.
Não crie uma política chamada default que simplesmente devolve a string. Isso silencia o mecanismo sem tornar o conteúdo seguro.
Use Trusted Types como defesa adicional, quando o ambiente permitir
Trusted Types reduz a área de revisão ao exigir valores tipados em sinks cobertos. O enforcement depende da diretiva CSP require-trusted-types-for 'script' e ainda deve ser verificado nos navegadores suportados.
No contrato atual do Site no Ar, a Version publicada não transforma um arquivo _headers em cabeçalhos HTTP. Portanto, não declare Trusted Types em enforcement só porque a diretiva aparece na fonte. A camada que controla a resposta precisa ser confirmada e o header servido deve ser inspecionado.
O artigo de Content Security Policy explica como começar em report-only, validar jornadas e separar a política proposta da proteção realmente entregue.
Mesmo com enforcement, continue removendo sinks desnecessários. CSP e Trusted Types são defesa em profundidade, não licença para espalhar parsing de HTML.
Revise bibliotecas e código gerado
Frameworks costumam escapar interpolação textual por padrão, mas oferecem escapes explícitos para HTML bruto. Procure APIs equivalentes a “dangerously set HTML”, templates não escapados, renderizadores Markdown, editores ricos e plugins que montam DOM.
Para cada ocorrência:
- identifique a origem dos dados;
- confirme a versão da biblioteca;
- leia o contrato de escaping;
- verifique se há bypass intencional;
- teste o HTML final, não apenas o componente-fonte;
- confira se a build inclui outra cópia ou caminho legado.
Um agente pode gerar um helper seguro e depois contorná-lo em outro arquivo. A revisão precisa cobrir o artefato publicável e os chunks carregados pela página. O guia de dependências JavaScript ajuda a relacionar biblioteca, versão, bundle e atualização.
Teste sem executar payload destrutivo
Use marcadores inofensivos em um ambiente autorizado. O objetivo é provar se a entrada vira nó ou atributo inesperado, não demonstrar impacto em pessoas ou sistemas reais.
Teste pelo menos:
| Entrada | Evidência esperada |
|---|---|
| query string | caracteres aparecem como texto |
| fragmento da URL | nenhum HTML inesperado é criado |
| resposta de API | esquema validado antes da renderização |
| armazenamento local | valor adulterado falha de forma segura |
postMessage |
origem e formato são verificados |
| URL de link ou imagem | protocolo e destino passam por allowlist |
| conteúdo rico permitido | elementos e atributos fora da política são removidos |
| navegador sem defesa CSP | código continua seguro pela construção do DOM |
No DevTools, use breakpoints de modificação do DOM e acompanhe a origem do valor. Revise console, Elements, Network e Sources. Um scanner pode localizar padrões, mas só o rastreamento confirma se existe caminho entre fonte e sink.
Valide a URL que será entregue
O contrato de Publicação envia os arquivos estáticos preparados pelo projeto. Ele não reescreve JavaScript inseguro nem sanitiza respostas que a página buscará depois.
Antes do deploy:
- gere a build final;
- busque sinks também nos assets produzidos;
- confirme que source maps e arquivos de teste não serão publicados;
- execute os fluxos com entradas sintéticas;
- verifique formulário, Analytics e integrações;
- registre qualquer sink inevitável e sua fronteira de proteção.
Depois da publicação, repita os testes na URL padrão e em cada domínio próprio ativo. O HTML pode ser o mesmo, mas origem, integrações, CSP e comportamento de URLs podem variar por hostname.
Se a correção alterar JavaScript, siga o fluxo de republicação controlada e compare a versão anterior com a nova antes de considerar o risco encerrado.
Prompt copiável para o agente
Revise este site para DOM XSS sem corrigir automaticamente.
Aplicação publicável: [caminho]Comando de build: [comando]URLs autorizadas para teste: [lista]
- Inventarie fontes como URL, formulário, API, storage e postMessage.- Busque sinks de HTML, script e navegação no código e na build.- Mostre cada caminho fonte -> transformação -> sink.- Prefira textContent e criação explícita de elementos para texto.- Valide protocolo e origem antes de atribuir URLs.- Para HTML necessário, identifique sanitizador, allowlist e ponto de uso.- Não crie política Trusted Types que apenas devolve a entrada.- Não trate CSP como correção do fluxo vulnerável.- Use somente marcadores sintéticos e inofensivos nos testes.- Devolva evidências, menor diff proposto e riscos restantes.Checklist final
- fontes controláveis foram inventariadas;
- sinks foram buscados na fonte e na build;
- cada ocorrência tem fluxo e contexto documentados;
- texto não confiável usa API de texto;
- estruturas são criadas com elementos conhecidos;
- URLs passam por validação de protocolo e destino;
- HTML rico existe apenas quando é requisito;
- sanitização é centralizada, mantida e testada;
- Trusted Types não usa política permissiva;
- CSP foi tratada como defesa adicional;
- testes usaram entradas sintéticas e inofensivas;
- URL padrão e domínios próprios foram verificados;
- artefato publicado corresponde à build revisada.
DOM XSS deixa de ser um padrão escondido no JavaScript quando a equipe consegue desenhar cada fluxo entre entrada e interpretação. Quanto menos strings chegam a APIs poderosas, menor é a superfície que precisa de sanitização, política e vigilância depois da publicação.
