XSS (Cross-Site Scripting): o que é e por que SPAs são alvo frequente
XSS é uma classe de vulnerabilidade em que um atacante consegue fazer o navegador executar JavaScript malicioso dentro do contexto do seu domínio. Isso é crítico porque o script roda com as permissões do usuário logado: pode ler dados exibidos na tela, capturar interações, disparar requisições em nome do usuário e, dependendo de como sua aplicação lida com credenciais, até exfiltrar informações sensíveis.
Em SPAs com React, existe uma proteção importante por padrão: ao renderizar valores em JSX (por exemplo, {user.name}), o React faz escaping de caracteres e evita que HTML seja interpretado como markup.
Porém, XSS ainda é possível quando você: injeta HTML diretamente, usa bibliotecas que inserem HTML no DOM, monta URLs perigosas, ou confia em dados não confiáveis para atributos e estilos.
Principais vetores de XSS em front-end
- HTML não sanitizado renderizado via
dangerouslySetInnerHTMLou via bibliotecas de markdown/HTML. - URLs controladas pelo usuário em
href/src(ex.:javascript:), ou redirecionamentos abertos. - Injeção em atributos (ex.:
on*handlers) quando se usa APIs de DOM diretamente. - DOM XSS ao usar
innerHTML,insertAdjacentHTMLou construir HTML com concatenação de strings. - Dependências vulneráveis (bibliotecas de UI, markdown, editores WYSIWYG) que permitem payloads.
Como evitar XSS no React: práticas recomendadas
1) Prefira renderização segura (JSX) e evite HTML bruto
Regra prática: se o conteúdo veio do usuário (comentários, descrições, campos rich text) ou de uma API que pode conter dados não confiáveis, não renderize como HTML. Renderize como texto com JSX e deixe o React escapar.
// Seguro: React escapa automaticamente o conteúdo textual
function Comment({ text }) {
return <p>{text}</p>;
}
2) Se precisar renderizar HTML, sanitize de forma explícita
Há casos legítimos: conteúdo de CMS, e-mails, artigos com formatação. Nesses casos, sanitize antes de renderizar. Uma abordagem comum é usar uma biblioteca de sanitização (ex.: DOMPurify). A sanitização deve ocorrer o mais próximo possível do ponto de renderização e com uma política restritiva (permitir apenas tags/atributos necessários).
// Exemplo conceitual (instale e configure uma biblioteca de sanitização)
import DOMPurify from 'dompurify';
function SafeHtml({ html }) {
const clean = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
Boas práticas adicionais ao sanitizar:
Continue em nosso aplicativo
Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.
ou continue lendo abaixo...Baixar o aplicativo
- Bloqueie URLs perigosas em atributos como
hrefesrc(ex.:javascript:,data:quando não necessário). - Evite permitir atributos de evento (
onclick, etc.). Sanitizadores bons já removem, mas valide. - Não confie em “sanitize no back-end” apenas. É útil, mas o front-end ainda deve tratar como não confiável.
3) Valide e normalize URLs antes de usar em links e redirecionamentos
Mesmo sem dangerouslySetInnerHTML, você pode criar problemas ao aceitar uma URL externa e colocá-la em href ou ao implementar redirecionamento pós-login. A prática é: permitir apenas caminhos internos (relativos) ou uma lista de domínios confiáveis.
// Permite apenas returnUrl interno (evita open redirect)
function safeReturnUrl(returnUrl) {
if (!returnUrl) return '/';
// só aceita caminhos relativos iniciando com '/'
if (typeof returnUrl === 'string' && returnUrl.startsWith('/')) return returnUrl;
return '/';
}
Para links externos fornecidos por usuários, prefira não renderizar como link clicável automaticamente. Se for necessário, normalize e valide o protocolo:
function isSafeHttpUrl(value) {
try {
const url = new URL(value);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch {
return false;
}
}
4) Evite APIs de DOM que inserem HTML
Em React, quase sempre há alternativa declarativa. Evite element.innerHTML = ... e similares. Se precisar integrar com bibliotecas legadas, isole em um componente e sanitize a entrada.
5) Use Content Security Policy (CSP) para reduzir impacto
CSP é um cabeçalho HTTP configurado no servidor que restringe de onde scripts, estilos, imagens e conexões podem ser carregados. Mesmo que um XSS aconteça, uma CSP bem configurada pode impedir execução de scripts injetados (especialmente se você bloquear unsafe-inline e usar nonces/hashes).
Como prática, alinhe com o time de back-end/infra para:
- Bloquear scripts inline e usar nonce/hashes quando necessário.
- Restringir
connect-srcaos domínios de API esperados. - Restringir
img-srceframe-srcpara evitar exfiltração e clickjacking.
No front-end, isso impacta como você inclui scripts e como lida com estilos inline. Planeje cedo para não depender de inline scripts.
CSRF com cookies: quando vira problema em SPAs
CSRF (Cross-Site Request Forgery) ocorre quando um site malicioso faz o navegador do usuário enviar uma requisição para o seu site, aproveitando que o navegador inclui automaticamente cookies de sessão. Isso é especialmente relevante quando sua autenticação usa cookies (por exemplo, cookie de sessão ou refresh token em cookie) e sua API aceita requisições state-changing (POST/PUT/PATCH/DELETE) sem uma verificação adicional.
Importante: CSRF não “rouba” o cookie; ele explora o fato de o cookie ser enviado automaticamente. Se sua API confia apenas no cookie para autenticar, um atacante pode induzir o navegador a disparar uma ação (ex.: trocar e-mail, fazer uma transferência, apagar um recurso) se não houver proteção.
Quando CSRF é menos relevante
- Quando a API exige um token no header (ex.:
Authorization: Bearer ...) e você não armazena esse token em cookies enviados automaticamente. Nesse caso, o atacante não consegue anexar o header por limitações do navegador (CORS e impossibilidade de ler tokens). - Quando endpoints sensíveis exigem confirmação adicional (reautenticação, MFA, senha), reduzindo impacto.
Quando CSRF é relevante
- Quando você usa cookies para autenticação (sessão, access token em cookie, refresh token em cookie) e o navegador envia cookies automaticamente.
- Quando você permite requisições cross-site com credenciais de forma permissiva.
Mitigações de CSRF em SPAs com cookies: checklist prático
1) Configure cookies com SameSite, Secure e HttpOnly
Essas flags são definidas no servidor ao emitir o cookie, mas o front-end precisa entender o efeito para não quebrar fluxos:
- SameSite=Lax: geralmente a melhor opção padrão. Cookies não são enviados em muitas requisições cross-site, reduzindo CSRF. Pode afetar alguns fluxos de login via redirecionamento.
- SameSite=Strict: mais restritivo; pode quebrar navegação legítima vinda de links externos.
- SameSite=None; Secure: necessário para cenários cross-site (ex.: app em domínio diferente da API) quando você precisa enviar cookies. Exige HTTPS e aumenta a necessidade de anti-CSRF.
- HttpOnly: impede leitura do cookie via JavaScript, reduzindo impacto de XSS para roubo de cookie (mas não impede CSRF).
- Secure: cookie só via HTTPS.
Se seu front-end e API estão em domínios diferentes e você usa cookies, é comum precisar de SameSite=None; Secure. Nesse cenário, trate anti-CSRF como obrigatório.
2) Use token anti-CSRF (double submit ou sincronizador)
Uma estratégia comum é o servidor emitir um token CSRF e o front-end enviá-lo em um header customizado (ex.: X-CSRF-Token) em requisições mutáveis. O atacante não consegue ler o token (por SOP/CORS), então não consegue forjar o header corretamente.
Passo a passo típico (visão SPA):
- 1) Ao carregar a aplicação (ou após login), o front-end chama um endpoint que retorna um token CSRF (ou o servidor o coloca em um cookie não-HttpOnly).
- 2) O front-end armazena o token em memória (ou em storage, dependendo do modelo) e passa a enviá-lo em headers para POST/PUT/PATCH/DELETE.
- 3) O servidor valida o token e rejeita se ausente ou inválido.
// Exemplo com fetch: incluir CSRF em requisições mutáveis
let csrfToken = null;
export async function initCsrf() {
const res = await fetch('/api/csrf', { credentials: 'include' });
const data = await res.json();
csrfToken = data.csrfToken;
}
export async function apiFetch(url, options = {}) {
const method = (options.method || 'GET').toUpperCase();
const headers = new Headers(options.headers || {});
const isMutating = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
if (isMutating && csrfToken) {
headers.set('X-CSRF-Token', csrfToken);
}
return fetch(url, {
...options,
headers,
credentials: 'include',
});
}
Observações importantes:
- Se o token CSRF expirar, trate 403/419 e reexecute
initCsrf()antes de repetir a requisição (com cuidado para não criar loops). - Não use o mesmo token para sempre; rotacione conforme política do servidor.
3) CORS e credenciais: seja restritivo
Se você usa cookies cross-site, o front-end precisa enviar credentials: 'include' (fetch) ou withCredentials: true (axios). Isso aumenta a superfície: o servidor deve permitir apenas origens específicas e nunca usar curingas com credenciais.
Do lado do front-end, boas práticas:
- Centralize a configuração de cliente HTTP para não “vazar”
credentialspara domínios indevidos. - Evite fazer chamadas para URLs arbitrárias controladas por usuário.
4) Proteja endpoints sensíveis com confirmação adicional
Mesmo com anti-CSRF, ações críticas (alterar e-mail, trocar senha, excluir conta, operações financeiras) devem pedir reautenticação, senha ou MFA. Isso reduz impacto de qualquer falha residual (CSRF, sessão sequestrada, dispositivo comprometido).
Hardening de UI: reduzindo vazamento de dados e ações indevidas no front-end
Hardening de UI é o conjunto de práticas para tornar a interface mais resistente a abuso, engenharia social e falhas de implementação que expõem dados ou permitem ações inesperadas. Não substitui validações do back-end, mas reduz risco e melhora a postura de segurança.
1) Não confie em “esconder” elementos como controle de acesso
Ocultar botões e rotas melhora UX, mas não é segurança. Ainda assim, é importante para reduzir tentativas acidentais e diminuir exposição de funcionalidades. A regra é: UI pode refletir permissões, mas o servidor deve impor.
Prática recomendada no front-end:
- Renderize ações apenas quando o usuário tem permissão.
- Desabilite e explique (tooltip/mensagem) quando a ação existe mas não está disponível.
- Evite enviar requisições “para ver se pode” em massa; isso vira enumeração.
2) Evite vazar informações sensíveis em mensagens e logs
Erros detalhados ajudam desenvolvimento, mas podem expor dados em produção. Cuidados:
- Não exiba mensagens do servidor diretamente se elas podem conter detalhes internos.
- Não logue tokens, headers de autenticação, payloads sensíveis ou dados pessoais no console.
- Em ferramentas de monitoramento no front-end, aplique mascaramento (redaction) para campos sensíveis.
// Exemplo: sanitizar logs de erro
function safeLogError(err) {
const message = err?.message || 'Erro';
// Evite imprimir objetos inteiros de request/response com headers
console.error('[UI Error]', message);
}
3) Proteja contra clickjacking (defesa em profundidade)
Clickjacking ocorre quando sua aplicação é carregada dentro de um iframe de um site malicioso, que sobrepõe elementos para induzir cliques. A mitigação principal é via headers (X-Frame-Options ou CSP frame-ancestors) configurados no servidor. No front-end, evite depender de “frame busting” via JS, mas você pode adicionar detecção como camada extra em apps internos.
// Camada extra (não substitui headers): detectar se está em iframe
if (window.top !== window.self) {
// Opcional: mostrar tela de bloqueio
document.body.innerHTML = '';
}
4) Controle de foco, teclado e estados para evitar ações acidentais
Alguns bugs de UI viram incidentes: duplo clique que dispara duas compras, botões ativos durante carregamento, formulários que submetem repetidamente. Hardening inclui:
- Desabilitar botões durante requisições.
- Implementar idempotência no cliente quando possível (ex.: bloquear múltiplos submits).
- Confirmar ações destrutivas com diálogos claros.
- Evitar atalhos perigosos sem confirmação.
// Exemplo: prevenir múltiplos submits
function SaveButton({ onSave }) {
const [loading, setLoading] = React.useState(false);
async function handleClick() {
if (loading) return;
setLoading(true);
try {
await onSave();
} finally {
setLoading(false);
}
}
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Salvando...' : 'Salvar'}
</button>
);
}
5) Trate arquivos e uploads com cuidado na UI
Uploads são um vetor comum para abuso (arquivos gigantes, tipos inesperados). Mesmo que o servidor valide, a UI deve:
- Restringir tipos aceitos (
accept) e validar tamanho antes de enviar. - Não renderizar “pré-visualização” de HTML/SVG não confiável como markup.
- Ao exibir arquivos enviados, preferir servir de um domínio separado (decisão de arquitetura) e usar headers adequados.
// Exemplo: validação básica antes do upload
function validateFile(file) {
const maxBytes = 5 * 1024 * 1024;
const allowed = ['image/jpeg', 'image/png'];
if (!allowed.includes(file.type)) throw new Error('Tipo de arquivo não permitido');
if (file.size > maxBytes) throw new Error('Arquivo excede o tamanho máximo');
}
6) Evite expor dados em caches e histórico quando não necessário
SPAs podem manter dados em memória, cache de requests e até persistir em storage. Para hardening:
- Evite persistir PII desnecessária em
localStorage/sessionStorage. - Ao lidar com telas sensíveis, considere limpar estados ao sair da rota.
- Evite colocar dados sensíveis em query string (ela vai para histórico, logs e referer).
7) Proteja a UI contra enumeração e “user discovery”
Mensagens como “usuário não existe” vs “senha incorreta” podem facilitar enumeração. Mesmo que a decisão final seja do back-end, a UI pode padronizar mensagens para não amplificar o vazamento:
- Use mensagens genéricas em autenticação (“Credenciais inválidas”).
- Implemente delays e limites de tentativa no servidor; no cliente, evite feedback excessivamente detalhado.
Passo a passo: auditoria rápida de XSS, CSRF e hardening em uma SPA React
Passo 1: mapear pontos de entrada de conteúdo não confiável
- Liste componentes que exibem texto vindo de usuários (comentários, perfis, descrições).
- Procure uso de
dangerouslySetInnerHTMLe bibliotecas de markdown/WYSIWYG. - Procure manipulação direta do DOM (
innerHTML,insertAdjacentHTML).
Passo 2: corrigir renderização de HTML
- Substitua HTML bruto por renderização em JSX quando possível.
- Quando HTML for necessário, sanitize com política restritiva e testes com payloads comuns.
Passo 3: revisar links, redirecionamentos e URLs
- Garanta que
returnUrle redirecionamentos aceitam apenas caminhos internos. - Valide URLs externas e bloqueie protocolos não HTTP(S).
- Em links externos, use
rel="noopener noreferrer"quandotarget="_blank"for usado para reduzir riscos de tabnabbing.
Passo 4: se usar cookies, implementar anti-CSRF
- Confirme com o back-end o modelo: SameSite, HttpOnly, Secure.
- Implemente obtenção do token CSRF e envio em header para métodos mutáveis.
- Centralize isso no cliente HTTP (wrapper fetch/axios) e trate expiração do token CSRF.
Passo 5: endurecer UI para ações críticas
- Bloqueie múltiplos submits e adicione confirmações para ações destrutivas.
- Padronize mensagens de erro para não vazar detalhes.
- Revise telas que exibem PII e evite persistência desnecessária.
Passo 6: alinhar headers de segurança com infra/back-end
Mesmo sendo um capítulo focado em front-end, a segurança real depende de headers e políticas. Alinhe para habilitar:
- CSP (com política compatível com sua build).
- frame-ancestors/X-Frame-Options para clickjacking.
- Referrer-Policy para reduzir vazamento via referer.
- Permissions-Policy para limitar APIs do navegador quando aplicável.