
← Voltar ao blog
Como o XSS atinge o DOM, quais APIs de navegador são pontos de entrada 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, pontos de entrada, correções que funcionam — além de um demo executável (playgrounds) (Vite + React em main; ao vivo: playgrounds.lucascoliveira.com) e ponteiros para esse 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 cookies forem enviados e 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 ponto de entrada 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). Cada usuário que carrega esse registro atinge o ponto de entrada. Impacto no 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 é refletida pelo servidor ou camada de roteamento na resposta. Clássico: ?q=<script>…</script> refletido no HTML sem codificação. Equivalente de SPA: location.search ou hash analisado 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 ponto de entrada. Exemplo: eval("handle" + location.hash.slice(1)) ou element.innerHTML = decodeURIComponent(...). A análise estática de modelos não é suficiente; você precisa auditar cada caminho de entrada não confiável para ponto de entrada.
Esses são os usuais culpados em bases de código React/SPA:
| Ponto de entrada | Risco |
|---|---|
element.innerHTML, insertAdjacentHTML | Parseia HTML; qualquer tag/ manipulador de evento que você permita pode executar script. |
dangerouslySetInnerHTML | Igual ao acima — React não sanitiza. |
document.write | Igual. |
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 ponto de entrada sem um contrato seguro — não apenas erros de “janela errada”. |
Não é um ponto de entrada por padrão: textContent, createTextNode, filhos de texto normais do React, atributos que o React trata como strings quando você não ignora sua escapada. Pipelines de Markdown se tornam pontos de entrada quando emitem HTML bruto e você atribui esse HTML ao DOM sem sanitização.
Nota do demo (importante): No 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 dependem de manipuladores de atributos (por exemplo, onerror em img) ou similares. O playground de XSS mantém cargas úteis baseadas em presets (veja payloadPresets.ts) para que você possa testar casos que realmente disparam após a inserção.
Se a UI só precisa 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, escapada de entidades 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 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 — Despoje valores href que começam com javascript: ou tipos MIME data: estranhos se você permitir links.No playgrounds em main, o playground de XSS contrasta um caminho de renderização inseguro com um caminho sanitizado (lista de permissões do DOMPurify) — mesma UI, política de confiança diferente.
CSP reduz o que pode ser executado quando algo passar 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 App Router do Next.js + 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 eventos 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 credentials explícita 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 (dev 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 playgrounds, execute npm install e npm run dev → Vite serve o aplicativo em http://localhost:5173.
/insecure com cargas úteis dos documentos; o mesmo fluxo em /secure mostra saída sanitizada.payloadPresets.ts e simulateXssImpact.ts para os casos concretos e mapeamento de “impacto”.localStorage (veja 03-session-hijacking) para que você possa ver o que o script na página pode ler; insecure-patterns também sugere localStorage.getItem('auth_token') no DevTools para inspecioná-lo.Compare /insecure vs /secure em Elements 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> como muitas folhas 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 backend; assuma que XSS e CSRF se encaixam.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 playground torna essa fronteira visível: um caminho de renderização inseguro vs um sanitizado, mais cargas úteis baseadas em presets e comportamento de “impacto” mapeado no repositório.