
← Voltar ao blog
Como o XSS chega ao DOM, quais APIs de navegador são sinks e mitigação que funcionam em produção — sanitização, CSP, cookies e pareamento de CSRF.
Se uma string for interpretada como markup ou script em vez de texto inerte, esse conteúdo é executado como código na origem da sua página — com os mesmos privilégios do seu pacote. Isso é XSS: não um bug de estilo, mas um delegado confuso entre dados e instruções. Este post é a espinha dorsal técnica — mecânicas de ataque, sinks, correções que funcionam — além de um demo executável frontend-xss-demo (Vite + React em main; ao vivo: xss.lucascoliveira.com) e ponteiros para este repositório Next.js onde cabeçalhos e MDX reduzem o risco padrão.
Scripts injetados na sua página são executados com a mesma origem do seu pacote. Na prática, isso geralmente significa:
document.cookie para cookies não marcados como HttpOnly (roubo de sessão / encadeamento de fixação).fetch() / XMLHttpRequest para sua API com credenciais ambientes se os cookies forem enviados e o CORS permitir — ou exfiltração de tokens mantidos em localStorage / sessionStorage se sua aplicação os colocar lá.Portanto, a mitigação não é “use React” — é nunca atribuir bytes controlados pelo atacante a um sink que parseia HTML ou executa script, a menos que um pipeline revisado os reduza a um tipo seguro primeiro.
XSS armazenado — HTML não confiável (ou uma carga útil que se torna HTML após expansão de modelo) é persistido (DB, cache, índice de pesquisa). Todo usuário que carrega esse registro atinge o sink. Impacto de frontend: a primeira renderização que faz innerHTML = row.body ou equivalente sem sanitização executa a carga útil.
XSS refletido — A carga útil nunca persiste; ela reflete do servidor ou camada de roteamento para a resposta. Clássico: ?q=<script>…</script> refletido no HTML sem codificação. Equivalente de SPA: location.search ou hash parseado no lado do cliente e escrito no DOM sem codificação. A correção é a mesma: tratar strings derivadas de URL como dados; se você deve refletir, codifique para o contexto (veja abaixo).
XSS baseado em DOM — A resposta do servidor é “limpa”, mas script do lado do cliente lê entrada controlada pelo atacante (location, referrer, postMessage, mensagens WebSocket) e passa para um sink. Exemplo: eval("handle" + location.hash.slice(1)) ou element.innerHTML = decodeURIComponent(...). Análise estática de modelos não é suficiente; você precisa auditar todo caminho de entrada não confiável para sink.
Esses são os usuais culpados em bases de código React/SPA:
| Sink | Risco |
|---|---|
element.innerHTML, insertAdjacentHTML | Parseia HTML; qualquer tag/ manipulador de evento que você permita pode executar script. |
dangerouslySetInnerHTML | Mesmo que acima — React não sanitiza. |
document.write | Mesmo. |
eval, new Function, setTimeout(string) | Execução direta de script. |
URLs javascript: em href / src | Navegação ou carregamento de recurso que executa como URL de script. |
Manipuladores postMessage que eval ou definem HTML a partir de event.data | XSS se origin não for validado ou event.data atingir um sink sem um contrato seguro — não apenas erros de “janela errada”. |
Não é um sink por padrão: textContent, createTextNode, texto de filho normal do React, atributos que o React trata como strings quando você não ignora sua escapada. Pipelines de Markdown se tornam sinks quando emitem HTML bruto e você atribui esse HTML ao DOM sem sanitização.
Nota do demo (importante): Em HTML5, nós <script> inseridos via innerHTML / dangerouslySetInnerHTML não são executados — o parser não os executa como um XSS refletido clássico. Para ver execução quando HTML é injetado, cargas úteis geralmente contam com manipuladores de atributos (por exemplo, onerror em img) ou similares. O documento insecure-patterns no repositório do demo explica isso para que você teste a aplicação com exemplos que realmente disparam após a inserção.
Se a UI precisar apenas de texto simples — Vincule texto com textContent, filhos de texto do React ou MDX que compila para componentes sem um pipeline de HTML. Nenhum sanitizador necessário; você não está no jogo de HTML.
Se você precisar de texto rico (negrito, listas, links) — Você precisa ou de uma linguagem de marcação restrita compilada para elementos seguros ou de sanitização de HTML com uma lista de permissões (tags + atributos). Codificação (por exemplo, escapamento de entidade HTML) é para colocar dados em nós de texto HTML; sanitização é para quando você deve permitir um subconjunto de HTML. Não confunda os dois.
Defesa em profundidade para texto rico em produtos reais — Valide/sanitiza ao gravar (API rejeita tags desconhecidas, limites de comprimento) e sanitiza ou renderiza por um caminho seguro ao ler (camada de renderização). O armazenamento pode ser revertido, corrompido ou escrito por outra versão de serviço.
DOMPurify é um sanitizador de navegador com um perfil padrão; você ainda configura para o seu produto:
ALLOWED_TAGS / ALLOWED_ATTR — Comece mínimo (p, br, strong, em, a com href somente se você precisar de links). Cada tag extra é uma superfície de ataque.ADD_ATTR / FORBID_TAGS — Explícito supera “permitir quase tudo”.RETURN_DOM / RETURN_TRUSTED_TYPE — Prefira nós DOM ou saída estilo TrustedHTML se você integrar com Tipos Confiáveis.afterSanitizeAttributes — Desfaça valores de href que começam com javascript: ou tipos MIME data: estranhos se você permitir links.No frontend-xss-demo em main, a rota segura (/secure) passa texto de todo para DOMPurify antes de dangerouslySetInnerHTML; a rota insegura (/insecure) não — mesma UI, política de confiança diferente. Português /seguro e /inseguro ainda existem como aliases legados e redirecionam para /secure e /insecure.
CSP reduz o que pode ser executado quando algo passa por. Em apps/web/next.config.ts este site define um CSP com default-src 'self', object-src 'none' restrito, base-uri 'self', form-action 'self', frame-ancestors 'none', além de script-src / style-src com 'unsafe-inline' porque o Next.js App Router + MUI sx atualmente precisam de script/style inline nesta configuração — documentado no código. Nonce- ou hash-based script-src removeria a permissão de script inline ampla, mas requer middleware para injetar nonces por solicitação — vale a pena planejar se você envia HTML de usuário raramente.
Verificação de realidade: CSP não substitui sanitização para HTML de usuário; ele estreita o raio de explosão (por exemplo, pode bloquear hosts de script que você não permitiu). Manipuladores de evento inline (onerror, etc.) não são automaticamente neutralizados apenas porque você define um CSP — 'unsafe-inline' em script-src é comum em aplicativos reais (incluindo a configuração Next/MUI deste site), e bloquear manipuladores geralmente requer script-src / script-src-attr explícito (ou nonces/hashes), dependendo do navegador e do nível de CSP.
XSS pode contornar tokens CSRF se o token for legível a partir do DOM ou se o script do atacante emitir solicitações com credenciais. Então: priorize correções XSS; também:
HttpOnly, Secure, SameSite=Lax ou Strict onde os fluxos permitem — reduz vazamento de cookie entre sites e CSRF clássico.SameSite, tokens anti-CSRF, ou headers personalizados + política CORS para que sites aleatórios não possam enviar solicitações credenciadas.Trabalho do frontend: não coloque segredos em armazenamento legível por JS se evitável; use fetch com política explícita de credentials alinhada com o design da sua API.
apps/web/next.config.ts: cabeçalhos de segurança em /(.*); string CSP construída em código com script-src específico do ambiente (desenvolvimento unsafe-eval para pilhas React somente onde necessário).apps/web/app/api/chat/route.ts: JSON parse, verificação vazia, limite MAX_MESSAGE_LENGTH — modelagem de abuso, não XSS por si só.apps/web/lib/blog/mdx.tsx: MDX com um mapa de componentes fixo (next-mdx-remote/rsc), não strings HTML brutas do CMS. Modelo de ameaça diferente de “corpo de mensagem com HTML.”main ofereceClone frontend-xss-demo, execute npm install e npm run dev → Vite serve a aplicação em http://localhost:5173.
/insecure com cargas úteis dos documentos; o mesmo fluxo em /secure mostra saída sanitizada.docs/xss/: uma visão geral, três caminhadas de impacto (ações sem interação do usuário, phishing interno, sequestro de sessão / armazenamento), além de padrões inseguros e padrões seguros para fazer e não fazer no nível de código.localStorage (veja 03-session-hijacking) para que você possa ver o que o script na página pode ler; padrões inseguros também sugere localStorage.getItem('auth_token') no DevTools para inspecioná-lo.Compare /insecure vs /secure em Elementos e Console: mesmos componentes, manipulação diferente da string antes de atingir o DOM.
rg "dangerouslySetInnerHTML|innerHTML|insertAdjacentHTML|eval\\(|new Function" no seu aplicativo e dependências.<img src=x onerror=...>, URLs javascript: — e lembre que innerHTML não executa <script> da maneira que muitos cartões de dicas implicam. Para demos de injeção de estilo innerHTML, prefira onerror em img (ou similar); <svg onload> é frequentemente não confiável quando inserido dessa maneira.SameSite, credenciais e estratégia CSRF com o backend; assuma que XSS e CSRF se encadeiam.XSS é controle de fluxo: dados cruzando para interpretação. A proteção é digitação na fronteira: texto simples, componentes estruturados seguros ou HTML sanitizado com uma lista de permissões mínima — mais CSP e semântica de cookies que limitam o que um script errante ainda pode fazer. O demo em main torna essa fronteira visível: /insecure vs /secure, impactos documentados sob docs/xss/, e disciplina em cada PR que toca strings perto do DOM.