Segurança em Aplicações React: XSS, CSRF e Content Security Policy na Prática Já leu

Introdução: O Cenário de Ameaças em Aplicações React Quando você constrói uma aplicação web com React, está criando interfaces que interagem com dados do usuário, APIs e bancos de dados. Essa interação constante cria superfícies de ataque que, se não forem adequadamente protegidas, podem comprometer a segurança de seus usuários e da aplicação inteira. React é uma biblioteca poderosa, mas não resolve magicamente problemas de segurança — cabe ao desenvolvedor implementar as camadas de proteção necessárias. As três ameaças que discutiremos — XSS (Cross-Site Scripting), CSRF (Cross-Site Request Forgery) e a ausência de uma política CSP (Content Security Policy) robusta — são responsáveis por uma parcela significativa dos ataques web documentados. A boa notícia é que com conhecimento sólido e práticas conscientes, você consegue mitigá-las de forma eficaz. XSS (Cross-Site Scripting): Injecting Malicious Code O que é XSS e por que é perigoso XSS ocorre quando um atacante consegue injetar código JavaScript malicioso que será executado no navegador de vítimas.

Introdução: O Cenário de Ameaças em Aplicações React

Quando você constrói uma aplicação web com React, está criando interfaces que interagem com dados do usuário, APIs e bancos de dados. Essa interação constante cria superfícies de ataque que, se não forem adequadamente protegidas, podem comprometer a segurança de seus usuários e da aplicação inteira. React é uma biblioteca poderosa, mas não resolve magicamente problemas de segurança — cabe ao desenvolvedor implementar as camadas de proteção necessárias.

As três ameaças que discutiremos — XSS (Cross-Site Scripting), CSRF (Cross-Site Request Forgery) e a ausência de uma política CSP (Content Security Policy) robusta — são responsáveis por uma parcela significativa dos ataques web documentados. A boa notícia é que com conhecimento sólido e práticas conscientes, você consegue mitigá-las de forma eficaz.

XSS (Cross-Site Scripting): Injecting Malicious Code

O que é XSS e por que é perigoso

XSS ocorre quando um atacante consegue injetar código JavaScript malicioso que será executado no navegador de vítimas. Existem três tipos principais: Stored XSS (o código malicioso é armazenado no servidor e entregue a todos os usuários), Reflected XSS (o código é refletido via URL ou parâmetros) e DOM-based XSS (a vulnerabilidade está na manipulação do DOM pelo cliente).

Em uma aplicação React, o risco aumenta quando você trata dados não sanitizados como conteúdo HTML. Um atacante poderia injetar <img src=x onerror="alert('XSS')" /> em um campo de comentário, e se seu aplicativo renderizar isso diretamente, o código executará nos navegadores de todos que virem esse comentário.

Proteção contra XSS no React

React já oferece proteção padrão contra XSS ao escapar automaticamente conteúdo dinâmico inserido via JSX. Quando você escreve:

const userComment = '<img src=x onerror="alert(\'XSS\')" />';

export function CommentDisplay() {
  return <div>{userComment}</div>;
}

React escapará o conteúdo e o renderizará como texto literal, não como HTML. Essa é a maior defesa do React contra XSS.

Porém, existem situações legítimas onde você precisa renderizar HTML. Use dangerouslySetInnerHTML apenas quando absolutamente necessário, e sempre sanitize o conteúdo com bibliotecas como DOMPurify:

import DOMPurify from 'dompurify';

export function RichTextComment({ htmlContent }) {
  const cleanHtml = DOMPurify.sanitize(htmlContent);

  return (
    <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />
  );
}

DOMPurify remove scripts, event handlers e outras estruturas perigosas, mantendo apenas as tags HTML seguras que você definir em uma whitelist. Sempre faça a sanitização no lado do cliente, mas nunca confie exclusivamente nela — valide e sanitize também no backend.

Outra prática importante é validar e escapar dados no servidor antes de enviá-los ao cliente. Se uma API retorna dados potencialmente contaminados, considere usar bibliotecas como html-entities para escapamento adicional:

import { decode } from 'html-entities';

export function SafeDisplay({ data }) {
  // Se o servidor retornou dados com HTML encoding,
  // decodifique-os de forma segura
  const safeText = decode(data);
  return <p>{safeText}</p>;
}

Lembre-se: confie sempre que há validação em múltiplas camadas. Uma validação apenas no frontend não é suficiente.

CSRF (Cross-Site Request Forgery): Forjando Requisições

Entendendo o ataque CSRF

CSRF é um ataque onde um site malicioso força seu navegador a fazer requisições em nome de um site legítimo onde você está autenticado. Imagine que você está autenticado em seu banco online em uma aba. Você abre outra aba e acessa um site malicioso que contém <img src="https://banco.com/transferir?valor=1000&para=atacante" />. Se seu navegador não tiver proteção, ele fará essa requisição usando seus cookies de autenticação.

O motivo pelo qual isso funciona é que cookies HTTP são enviados automaticamente com requisições cross-site (por padrão). O servidor não consegue distinguir se a requisição veio de uma ação legítima do usuário ou de um site malicioso.

Proteção contra CSRF com Tokens

A defesa padrão contra CSRF é usar tokens CSRF. O servidor gera um token único por sessão ou por requisição, o cliente deve incluir esse token em requisições que modificam dados (POST, PUT, DELETE), e o servidor valida o token antes de processar a ação.

Para implementar isso em React com uma API segura, primeiro você precisa que o servidor gere e retorne o token:

import { useEffect, useState } from 'react';

export function TransferForm() {
  const [csrfToken, setCsrfToken] = useState('');
  const [loading, setLoading] = useState(false);

  // Buscar o token CSRF quando o componente monta
  useEffect(() => {
    fetch('/api/csrf-token', {
      method: 'GET',
      credentials: 'include' // Importante: inclui cookies
    })
    .then(res => res.json())
    .then(data => setCsrfToken(data.token))
    .catch(err => console.error('Erro ao buscar token CSRF:', err));
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);

    try {
      const response = await fetch('/api/transfer', {
        method: 'POST',
        credentials: 'include', // Envia cookies de autenticação
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': csrfToken // Inclui o token no header
        },
        body: JSON.stringify({
          amount: 100,
          toAccount: '12345'
        })
      });

      if (response.ok) {
        alert('Transferência realizada com sucesso!');
      } else {
        alert('Erro na transferência');
      }
    } catch (error) {
      console.error('Erro:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="number" placeholder="Valor" required />
      <input type="text" placeholder="Conta destino" required />
      <button type="submit" disabled={loading || !csrfToken}>
        {loading ? 'Processando...' : 'Transferir'}
      </button>
    </form>
  );
}

No lado do servidor (exemplo com Express.js e middleware CSRF):

const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const express = require('express');

const app = express();
app.use(cookieParser());
app.use(express.json());

// Middleware CSRF
const csrfProtection = csrf({ cookie: false, sessionKey: 'session' });

// Retorna o token CSRF para o cliente
app.get('/api/csrf-token', csrfProtection, (req, res) => {
  res.json({ token: req.csrfToken() });
});

// Protege a rota de transferência
app.post('/api/transfer', csrfProtection, (req, res) => {
  // Token foi validado automaticamente pelo middleware
  // Se chegou aqui, é uma requisição legítima
  const { amount, toAccount } = req.body;

  // Processar transferência
  res.json({ success: true });
});

SameSite Cookies: Camada Adicional

Além de tokens CSRF, configure seus cookies com o atributo SameSite. Isso previne que cookies sejam enviados em requisições cross-site:

app.post('/api/login', (req, res) => {
  // ... validar credenciais ...

  res.cookie('sessionId', tokenValue, {
    httpOnly: true,      // Não acessível via JavaScript
    secure: true,        // Apenas HTTPS
    sameSite: 'Strict'   // Não envia em requisições cross-site
  });

  res.json({ success: true });
});

SameSite=Strict é a opção mais segura, mas pode afetar fluxos legítimos (como links de outros sites). Use SameSite=Lax para um equilíbrio entre segurança e usabilidade.

Content Security Policy (CSP): Estabelecendo Limites

Conceito e Importância da CSP

Content Security Policy é um header HTTP que define quais recursos (scripts, estilos, imagens, etc.) sua aplicação pode carregar. Ela funciona como uma whitelist que o navegador respeita, bloqueando qualquer recurso não autorizado.

Se um atacante conseguir injetar um <script> tag em sua página, ele será bloqueado se sua CSP não permitir scripts de fontes externas. Se ele tentar fazer uma requisição XHR para domínios não permitidos, o navegador bloqueará.

Implementando CSP no React

Configure o header CSP no seu servidor. Para uma aplicação React hospedada em app.exemplo.com que faz requisições para uma API em api.exemplo.com:

// Express.js
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; " +
    "script-src 'self' 'nonce-{random}'; " +
    "style-src 'self' 'unsafe-inline'; " +
    "connect-src 'self' https://api.exemplo.com; " +
    "img-src 'self' data: https:; " +
    "font-src 'self' data:; " +
    "frame-ancestors 'none'; " +
    "base-uri 'self'; " +
    "form-action 'self'"
  );
  next();
});

Nesta política:
- default-src 'self': Por padrão, permite recursos apenas do mesmo domínio
- script-src 'self' 'nonce-{random}': Scripts apenas do mesmo domínio ou com um nonce específico
- connect-src 'self' https://api.exemplo.com: Requisições HTTP apenas para esses domínios
- frame-ancestors 'none': Impede que sua aplicação seja embutida em iframes (previne clickjacking)
- form-action 'self': Formulários podem fazer submit apenas para o mesmo domínio

Para usar nonces (números usados uma única vez) que aumentam a segurança, gere um nonce único por requisição:

const crypto = require('crypto');

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('hex');
  res.locals.nonce = nonce;

  res.setHeader(
    'Content-Security-Policy',
    `default-src 'self'; ` +
    `script-src 'self' 'nonce-${nonce}'; ` +
    `style-src 'self' 'unsafe-inline'`
  );

  next();
});

Depois, no seu HTML/template, inclua o nonce nos scripts inline:

<!DOCTYPE html>
<html>
<head>
  <title>Minha App React</title>
</head>
<body>
  <div id="root"></div>
  <script nonce="<%= nonce %>">
    // JavaScript inline aqui é permitido
    ReactDOM.createRoot(document.getElementById('root')).render(
      <App />
    );
  </script>
</body>
</html>

Testando e Ajustando CSP

Para entender qual CSP sua aplicação precisa, use o modo "report-only" primeiro:

app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy-Report-Only',
    "default-src 'self'; " +
    "report-uri /csp-report"
  );
  next();
});

// Endpoint para receber relatórios de violação
app.post('/csp-report', express.json(), (req, res) => {
  console.log('CSP Violation:', req.body);
  res.sendStatus(204);
});

Isso permite que você monitore quais recursos estão causando problemas sem bloquear nada. Quando estiver confiante, mude para o header oficial.

Ferramentas como CSP Evaluator do Google ajudam a identificar fraquezas em sua política:

❌ Ruim:
Content-Security-Policy: default-src *

✅ Bom:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-randomvalue'

Conclusão

Ao dominar essas três camadas de segurança — proteção contra XSS através do escape automático do React e sanitização quando necessário, defesa contra CSRF com tokens e cookies SameSite, e restrição de recursos com CSP robusta — você cria uma aplicação significativamente mais segura. Não existe segurança perfeita, mas essas práticas implementadas juntas cobrem a maioria dos vetores de ataque comuns em aplicações web modernas.

O ponto crítico é entender que segurança é uma responsabilidade compartilhada entre frontend e backend. React protege você em algumas frentes, mas confiar exclusivamente nele é ingenuidade. Validação, sanitização e autenticação devem acontecer no servidor, enquanto o frontend adiciona camadas defensivas extras para melhorar a experiência do usuário e a resiliência geral.

Referências


Artigos relacionados