Segurança no 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 no frontend: XSS, limites de confiança e um demo que você pode executar

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.

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, 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.

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 cookies forem enviados e 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 aplicativo.

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, 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). 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.

Pontos de entrada: APIs que transformam strings em execução ou HTML

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

Ponto de entradaRisco
element.innerHTML, insertAdjacentHTMLParseia HTML; qualquer tag/ manipulador de evento que você permita pode executar script.
dangerouslySetInnerHTMLIgual ao acima — React não sanitiza.
document.writeIgual.
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 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.

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

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

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

Mitigação 3: Content-Security-Policy (limites, não uma substituição)

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.

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 credentials explícita 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 (dev 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 playgrounds, execute npm install e npm run devVite serve o aplicativo em http://localhost:5173.

  • Site ao vivo: playgrounds.lucascoliveira.com — mesmo demo que o repositório; use isso se você preferir não executar localmente.
  • UI de ToDo (somente na memória) — Adicione itens em /insecure com cargas úteis dos documentos; o mesmo fluxo em /secure mostra saída sanitizada.
  • Documentação / internos — Comece no README, então verifique payloadPresets.ts e simulateXssImpact.ts para os casos concretos e mapeamento de “impacto”.
  • Token falso — O aplicativo 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; 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.

Checklist (nível de implementação)

  1. Inventário de pontos de entradarg "dangerouslySetInnerHTML|innerHTML|insertAdjacentHTML|eval\\(|new Function" no seu aplicativo e dependências.
  2. Texto rico — Sanitizador de lista de permissões em cada caminho para HTML; teste de unidade com cargas úteis como <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.
  3. Parâmetros de URL → DOM — Nunca atribua pesquisa/hash a HTML; se você deve exibir, texto ou codifique para o contexto.
  4. Markdown — Sanitize 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 backend; assuma que XSS e CSRF se encaixam.

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 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.