Segurança de frontend: XSS, limites de confiança e um demo que você pode executar

← Voltar ao blog

20 de abril de 20268 min lido

Segurança de frontend: XSS, limites de confiança e um demo que você pode executar

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.

segurançafrontendreact

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.

Mesma origem: o que o atacante realmente ganha

Scripts injetados na sua página são executados com a mesma origem do seu pacote. Na prática, isso geralmente significa:

  • Visibilidade de 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á.
  • Leitura de DOM de PII não secreto renderizado na página e redirecionamento de UI ( formulários falsos, sobreposições) para phishing dentro do chrome da aplicação.

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, refletido e baseado em DOM (mecânicas)

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.

Sinks: APIs que transformam strings em execução ou HTML

Esses são os usuais culpados em bases de código React/SPA:

SinkRisco
element.innerHTML, insertAdjacentHTMLParseia HTML; qualquer tag/ manipulador de evento que você permita pode executar script.
dangerouslySetInnerHTMLMesmo que acima — React não sanitiza.
document.writeMesmo.
eval, new Function, setTimeout(string)Execução direta de script.
URLs javascript: em href / srcNavegação ou carregamento de recurso que executa como URL de script.
Manipuladores postMessage que eval ou definem HTML a partir de event.dataXSS 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.

Mitigação 1: codificação apropriada ao contexto vs sanitizaçã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.

Mitigação 2: DOMPurify (e como usá-lo seriamente)

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.
  • Enganche em 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.

Mitigação 3: Content-Security-Policy (limites, não um substituto)

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.

Mitigação 4: cookies e CSRF (par com XSS)

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:

  • Cookies de sessão: HttpOnly, Secure, SameSite=Lax ou Strict onde os fluxos permitem — reduz vazamento de cookie entre sites e CSRF clássico.
  • Endpoints de mutação: par com cookies 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.

Este codebase (concreto)

  • Cabeçalhos / CSPapps/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).
  • API de bate-papoapps/web/app/api/chat/route.ts: JSON parse, verificação vazia, limite MAX_MESSAGE_LENGTH — modelagem de abuso, não XSS por si só.
  • Blogapps/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.”

Comparação executável: o que main oferece

Clone frontend-xss-demo, execute npm install e npm run devVite serve a aplicação em http://localhost:5173.

  • Site ao vivo: xss.lucascoliveira.com — mesmo demo que o repositório; use isso se você preferir não executar localmente.
  • UI de Todo (somente memória) — Adicione itens em /insecure com cargas úteis dos documentos; o mesmo fluxo em /secure mostra saída sanitizada.
  • Documentação — Sob 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.
  • Token falso — A aplicação armazena um token de demo em 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.

Checklist (nível de implementação)

  1. Inventário de sinksrg "dangerouslySetInnerHTML|innerHTML|insertAdjacentHTML|eval\\(|new Function" no seu aplicativo e dependências.
  2. Texto rico — Sanitizador de lista de permissões em todo caminho para HTML; teste de unidade com cargas úteis como <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.
  3. Parâmetros de URL → DOM — Nunca atribua pesquisa/hash a HTML; se você deve exibir, texto ou codifique para o contexto.
  4. Markdown — Sanitiza após conversão completa de MD→HTML; proíba HTML bruto em MD se o produto permitir.
  5. CSP — Aperte incrementalmente; use Report-Only em staging se necessário.
  6. Cookies / API — Alinhe SameSite, credenciais e estratégia CSRF com o backend; assuma que XSS e CSRF se encadeiam.

O resumo

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.