React & Frontend

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

14 min de leitura

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

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.

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

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

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

<h2>XSS (Cross-Site Scripting): Injecting Malicious Code</h2>

<h3>O que é XSS e por que é perigoso</h3>

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

<p>Em uma aplicação React, o risco aumenta quando você trata dados não sanitizados como conteúdo HTML. Um atacante poderia injetar <code>&lt;img src=x onerror=&quot;alert(&#039;XSS&#039;)&quot; /&gt;</code> 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.</p>

<h3>Proteção contra XSS no React</h3>

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

<pre><code class="language-jsx">const userComment = &#039;&lt;img src=x onerror=&quot;alert(\&#039;XSS\&#039;)&quot; /&gt;&#039;;

export function CommentDisplay() {

return &lt;div&gt;{userComment}&lt;/div&gt;;

}</code></pre>

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

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

<pre><code class="language-jsx">import DOMPurify from &#039;dompurify&#039;;

export function RichTextComment({ htmlContent }) {

const cleanHtml = DOMPurify.sanitize(htmlContent);

return (

&lt;div dangerouslySetInnerHTML={{ __html: cleanHtml }} /&gt;

);

}</code></pre>

<p><code>DOMPurify</code> 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.</p>

<p>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 <code>html-entities</code> para escapamento adicional:</p>

<pre><code class="language-jsx">import { decode } from &#039;html-entities&#039;;

export function SafeDisplay({ data }) {

// Se o servidor retornou dados com HTML encoding,

// decodifique-os de forma segura

const safeText = decode(data);

return &lt;p&gt;{safeText}&lt;/p&gt;;

}</code></pre>

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

<h2>CSRF (Cross-Site Request Forgery): Forjando Requisições</h2>

<h3>Entendendo o ataque CSRF</h3>

<p>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 <code>&lt;img src=&quot;https://banco.com/transferir?valor=1000&amp;para=atacante&quot; /&gt;</code>. Se seu navegador não tiver proteção, ele fará essa requisição usando seus cookies de autenticação.</p>

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

<h3>Proteção contra CSRF com Tokens</h3>

<p>A defesa padrão contra CSRF é usar <strong>tokens CSRF</strong>. 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.</p>

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

<pre><code class="language-jsx">import { useEffect, useState } from &#039;react&#039;;

export function TransferForm() {

const [csrfToken, setCsrfToken] = useState(&#039;&#039;);

const [loading, setLoading] = useState(false);

// Buscar o token CSRF quando o componente monta

useEffect(() =&gt; {

fetch(&#039;/api/csrf-token&#039;, {

method: &#039;GET&#039;,

credentials: &#039;include&#039; // Importante: inclui cookies

})

.then(res =&gt; res.json())

.then(data =&gt; setCsrfToken(data.token))

.catch(err =&gt; console.error(&#039;Erro ao buscar token CSRF:&#039;, err));

}, []);

const handleSubmit = async (e) =&gt; {

e.preventDefault();

setLoading(true);

try {

const response = await fetch(&#039;/api/transfer&#039;, {

method: &#039;POST&#039;,

credentials: &#039;include&#039;, // Envia cookies de autenticação

headers: {

&#039;Content-Type&#039;: &#039;application/json&#039;,

&#039;X-CSRF-Token&#039;: csrfToken // Inclui o token no header

},

body: JSON.stringify({

amount: 100,

toAccount: &#039;12345&#039;

})

});

if (response.ok) {

alert(&#039;Transferência realizada com sucesso!&#039;);

} else {

alert(&#039;Erro na transferência&#039;);

}

} catch (error) {

console.error(&#039;Erro:&#039;, error);

} finally {

setLoading(false);

}

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;input type=&quot;number&quot; placeholder=&quot;Valor&quot; required /&gt;

&lt;input type=&quot;text&quot; placeholder=&quot;Conta destino&quot; required /&gt;

&lt;button type=&quot;submit&quot; disabled={loading || !csrfToken}&gt;

{loading ? &#039;Processando...&#039; : &#039;Transferir&#039;}

&lt;/button&gt;

&lt;/form&gt;

);

}</code></pre>

<p>No lado do servidor (exemplo com Express.js e middleware CSRF):</p>

<pre><code class="language-javascript">const csrf = require(&#039;csurf&#039;);

const cookieParser = require(&#039;cookie-parser&#039;);

const express = require(&#039;express&#039;);

const app = express();

app.use(cookieParser());

app.use(express.json());

// Middleware CSRF

const csrfProtection = csrf({ cookie: false, sessionKey: &#039;session&#039; });

// Retorna o token CSRF para o cliente

app.get(&#039;/api/csrf-token&#039;, csrfProtection, (req, res) =&gt; {

res.json({ token: req.csrfToken() });

});

// Protege a rota de transferência

app.post(&#039;/api/transfer&#039;, csrfProtection, (req, res) =&gt; {

// 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 });

});</code></pre>

<h3>SameSite Cookies: Camada Adicional</h3>

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

<pre><code class="language-javascript">app.post(&#039;/api/login&#039;, (req, res) =&gt; {

// ... validar credenciais ...

res.cookie(&#039;sessionId&#039;, tokenValue, {

httpOnly: true, // Não acessível via JavaScript

secure: true, // Apenas HTTPS

sameSite: &#039;Strict&#039; // Não envia em requisições cross-site

});

res.json({ success: true });

});</code></pre>

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

<h2>Content Security Policy (CSP): Estabelecendo Limites</h2>

<h3>Conceito e Importância da CSP</h3>

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

<p>Se um atacante conseguir injetar um <code>&lt;script&gt;</code> 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á.</p>

<h3>Implementando CSP no React</h3>

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

<pre><code class="language-javascript">// Express.js

app.use((req, res, next) =&gt; {

res.setHeader(

&#039;Content-Security-Policy&#039;,

&quot;default-src &#039;self&#039;; &quot; +

&quot;script-src &#039;self&#039; &#039;nonce-{random}&#039;; &quot; +

&quot;style-src &#039;self&#039; &#039;unsafe-inline&#039;; &quot; +

&quot;connect-src &#039;self&#039; https://api.exemplo.com; &quot; +

&quot;img-src &#039;self&#039; data: https:; &quot; +

&quot;font-src &#039;self&#039; data:; &quot; +

&quot;frame-ancestors &#039;none&#039;; &quot; +

&quot;base-uri &#039;self&#039;; &quot; +

&quot;form-action &#039;self&#039;&quot;

);

next();

});</code></pre>

<p>Nesta política:</p>

<ul>

<li><code>default-src &#039;self&#039;</code>: Por padrão, permite recursos apenas do mesmo domínio</li>

<li><code>script-src &#039;self&#039; &#039;nonce-{random}&#039;</code>: Scripts apenas do mesmo domínio ou com um nonce específico</li>

<li><code>connect-src &#039;self&#039; https://api.exemplo.com</code>: Requisições HTTP apenas para esses domínios</li>

<li><code>frame-ancestors &#039;none&#039;</code>: Impede que sua aplicação seja embutida em iframes (previne clickjacking)</li>

<li><code>form-action &#039;self&#039;</code>: Formulários podem fazer submit apenas para o mesmo domínio</li>

</ul>

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

<pre><code class="language-javascript">const crypto = require(&#039;crypto&#039;);

app.use((req, res, next) =&gt; {

const nonce = crypto.randomBytes(16).toString(&#039;hex&#039;);

res.locals.nonce = nonce;

res.setHeader(

&#039;Content-Security-Policy&#039;,

default-src &#039;self&#039;; +

script-src &#039;self&#039; &#039;nonce-${nonce}&#039;; +

style-src &#039;self&#039; &#039;unsafe-inline&#039;

);

next();

});</code></pre>

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

<pre><code class="language-html">&lt;!DOCTYPE html&gt;

&lt;html&gt;

&lt;head&gt;

&lt;title&gt;Minha App React&lt;/title&gt;

&lt;/head&gt;

&lt;body&gt;

&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;

&lt;script nonce=&quot;&lt;%= nonce %&gt;&quot;&gt;

// JavaScript inline aqui é permitido

ReactDOM.createRoot(document.getElementById(&#039;root&#039;)).render(

&lt;App /&gt;

);

&lt;/script&gt;

&lt;/body&gt;

&lt;/html&gt;</code></pre>

<h3>Testando e Ajustando CSP</h3>

<p>Para entender qual CSP sua aplicação precisa, use o modo &quot;report-only&quot; primeiro:</p>

<pre><code class="language-javascript">app.use((req, res, next) =&gt; {

res.setHeader(

&#039;Content-Security-Policy-Report-Only&#039;,

&quot;default-src &#039;self&#039;; &quot; +

&quot;report-uri /csp-report&quot;

);

next();

});

// Endpoint para receber relatórios de violação

app.post(&#039;/csp-report&#039;, express.json(), (req, res) =&gt; {

console.log(&#039;CSP Violation:&#039;, req.body);

res.sendStatus(204);

});</code></pre>

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

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

<pre><code class="language-plaintext"></code></pre>

<h2>Conclusão</h2>

<p>Ao dominar essas três camadas de segurança — <strong>proteção contra XSS através do escape automático do React e sanitização quando necessário</strong>, <strong>defesa contra CSRF com tokens e cookies SameSite</strong>, e <strong>restrição de recursos com CSP robusta</strong> — 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.</p>

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

<h2>Referências</h2>

<ul>

<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html" target="_blank" rel="noopener noreferrer">OWASP XSS Prevention Cheat Sheet</a></li>

<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html" target="_blank" rel="noopener noreferrer">OWASP CSRF Prevention Cheat Sheet</a></li>

<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP" target="_blank" rel="noopener noreferrer">MDN Web Docs - Content Security Policy</a></li>

<li><a href="https://react.dev/learn/security" target="_blank" rel="noopener noreferrer">React Official Security Documentation</a></li>

<li><a href="https://github.com/cure53/DOMPurify" target="_blank" rel="noopener noreferrer">DOMPurify - Documentation</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Como Usar useImperativeHandle e forwardRef: Expondo APIs de Componentes em Produção
Como Usar useImperativeHandle e forwardRef: Expondo APIs de Componentes em Produção

O Problema: Componentes como Caixas Pretas Em React, componentes funcionais s...

Guia Completo de Arquiteturas de Estado em React: Local, Global, Server e URL State
Guia Completo de Arquiteturas de Estado em React: Local, Global, Server e URL State

Introdução: Os Quatro Pilares do Gerenciamento de Estado O gerenciamento de e...

Guia Completo de Render Props em React: Quando Ainda Fazem Sentido
Guia Completo de Render Props em React: Quando Ainda Fazem Sentido

O Que São Render Props? Render Props é um padrão de design em React que consi...