Skip to main content

React Security: Combining CSRF, CORS, and CSP

No single security mechanism is perfect. CSRF tokens prevent form-based attacks but do not block script injection. CSP blocks injected scripts but does not prevent CSRF if misconfigurations exist. CORS restricts cross-origin access but requires the server to enforce it. Production React applications use all three—CSRF tokens + SameSite cookies, strict CORS headers, and a strict CSP—to create defense in depth. When one defense fails, the others remain. This article shows you how to layer these defenses in a cohesive security architecture and test the overall resilience of your React app.

Defense in depth is the principle that security relies on multiple independent layers, not a single mechanism (NIST, 2025).

Security Architecture: Layers and Responsibilities

Each defense mechanism protects against a different attack vector:

AttackCSRF TokensSameSite CookiesCORS HeadersCSPResult
Form-based CSRF from evil.comBlocks (token missing)Blocks (cookie not sent)N/AN/ADoubly blocked
XSS via injected <script>N/AN/AN/ABlocks (inline script)Blocked
Unauthorized API fetch from evil.comN/AN/ABlocks (header missing)N/ABlocked
Session cookie theft via XSSN/AMitigates (HttpOnly)N/ABlocks XSSDoubly blocked
Request with stolen token but wrong originValidates (token tied to session)Blocks (cookie not sent)N/AN/ADoubly blocked

A comprehensive React security architecture includes:

  1. Frontend (React Component Level): Validate user input, sanitize output, use HttpOnly cookies, use CSRF tokens in forms.
  2. Network Layer: CORS headers restrict cross-origin requests; CSRF tokens validate state-changing requests.
  3. Header-Based (HTTP/Browser Level): CSP blocks inline scripts; SameSite prevents accidental cookie leakage.
  4. Backend (Server Level): Validate all input, verify CSRF tokens, enforce authorization checks.
  5. Monitoring: Log CSP violations, track CORS rejections, alert on suspicious patterns.

A Complete Secure React + API Setup

Here is a fully integrated example combining all three defenses:

Backend (Node.js/Express):

const express = require('express');
const cors = require('cors');
const session = require('express-session');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Middleware 1: Session management with secure cookies
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: true, // Prevent JavaScript access (defend against XSS)
secure: true, // HTTPS only
sameSite: 'Strict', // Prevent CSRF via cookies
maxAge: 3600000, // 1 hour
},
}));

// Middleware 2: Generate and validate CSRF tokens
app.get('/api/csrf-token', (req, res) => {
const token = crypto.randomBytes(32).toString('hex');
req.session.csrfToken = token;
res.json({ token });
});

// Middleware 3: CORS with specific origins
const corsOptions = {
origin: ['https://app.example.com', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'X-CSRF-Token'],
credentials: true,
maxAge: 3600,
};
app.use(cors(corsOptions));

// Middleware 4: Validate CSRF tokens on state-changing requests
app.use((req, res, next) => {
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
const tokenFromRequest = req.headers['x-csrf-token'];
const tokenFromSession = req.session.csrfToken;

if (!tokenFromRequest || tokenFromRequest !== tokenFromSession) {
return res.status(403).json({ error: 'CSRF token invalid' });
}
}
next();
});

// Middleware 5: Set CSP header with nonce
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'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; object-src 'none'; report-to csp-endpoint`
);

res.setHeader(
'Reporting-Endpoints',
'csp-endpoint="https://example.com/csp-report"'
);

next();
});

// Protected API endpoint
app.post('/api/transfer', (req, res) => {
// By this point, CSRF token is validated, CORS is checked, session is valid
const user = req.session.user;
const { amount, to } = req.body;

if (!user) {
return res.status(401).json({ error: 'Not authenticated' });
}

// Process the transfer
console.log(`Transfer of ${amount} from ${user.id} to ${to}`);
res.json({ success: true });
});

// CSP violation reporting endpoint
app.post('/csp-report', express.json(), (req, res) => {
const reports = req.body || [];
reports.forEach(report => {
if (report.type === 'csp-violation') {
console.warn('CSP Violation:', report.body);
// Log to monitoring system
}
});
res.status(204).send();
});

app.listen(3001, () => {
console.log('Secure API listening on port 3001');
console.log('CSRF tokens: enabled');
console.log('SameSite cookies: Strict');
console.log('CORS: configured for specific origins');
console.log('CSP: enabled with nonce, script-src strict');
});

Frontend (React):

// React component with all three defenses
import React, { useState, useEffect } from 'react';

export default function SecureTransferForm() {
const [csrfToken, setCsrfToken] = useState(null);
const [amount, setAmount] = useState('');
const [to, setTo] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

// Step 1: Fetch CSRF token on mount
useEffect(() => {
fetch('/api/csrf-token', {
method: 'GET',
credentials: 'include', // Include session cookie
})
.then(res => res.json())
.then(data => setCsrfToken(data.token))
.catch(err => setError('Failed to load form: ' + err.message));
}, []);

// Step 2: Submit form with CSRF token
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);

try {
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken, // Include CSRF token
},
credentials: 'include', // Include session cookie
body: JSON.stringify({ amount, to }),
});

const result = await response.json();

if (response.ok) {
alert('Transfer successful!');
setAmount('');
setTo('');
} else {
setError(result.error || 'Transfer failed');
}
} catch (err) {
setError('Network error: ' + err.message);
} finally {
setLoading(false);
}
};

if (error) return <div style={{ color: 'red' }}>Error: {error}</div>;
if (!csrfToken) return <div>Loading...</div>;

return (
<form onSubmit={handleSubmit}>
<h2>Transfer Funds</h2>
<input
name="to"
placeholder="Recipient"
value={to}
onChange={e => setTo(e.target.value)}
required
/>
<input
name="amount"
type="number"
placeholder="Amount"
value={amount}
onChange={e => setAmount(e.target.value)}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Processing...' : 'Transfer'}
</button>
</form>
);
}

Testing Your Defenses

To verify all three mechanisms are working:

Test 1: CSRF Token Validation

Create a test form on a different origin and try to submit to your API. It should fail because the CSRF token is missing.

<!-- attacker.html on attacker.com -->
<form action="https://example.com/api/transfer" method="POST">
<input name="amount" value="5000">
<input name="to" value="[email protected]">
</form>
<script>document.querySelector('form').submit();</script>
<!-- Result: CSRF token missing, request rejected -->

Test 2: CORS Rejection

Try a fetch request from a different origin without proper CORS headers:

// From https://evil.com
fetch('https://api.example.com/api/transfer', {
method: 'POST',
body: JSON.stringify({ amount: 5000 }),
})
.catch(err => console.log('CORS blocked:', err));
// Result: CORS error in browser console

Test 3: CSP Script Blocking

Try to inject a script in your React app (in the browser console):

const script = document.createElement('script');
script.textContent = `alert('injected');`;
document.body.appendChild(script);
// Result: Blocked by CSP (script-src 'self'), no alert

Test 4: SameSite Cookie Behavior

Check that your session cookie is not sent with cross-origin requests:

// From https://evil.com
fetch('https://example.com/api/user', {
credentials: 'include',
})
.then(res => res.json())
.catch(err => console.log('Request failed (no cookie sent):', err));
// Result: Request reaches server but user is not authenticated (no cookie)

Key Takeaways

  • Defense in depth: combine CSRF tokens, SameSite cookies, CORS headers, and CSP.
  • Each mechanism protects a different attack vector; no single mechanism is sufficient.
  • CSRF tokens validate state-changing requests; SameSite cookies prevent accidental credential leakage.
  • CORS headers restrict cross-origin access; CSP blocks script injection.
  • Test each defense independently to ensure they work together.
  • Log and monitor violations; adjust policies based on real-world violations.

Frequently Asked Questions

If I have CSRF tokens, do I still need SameSite cookies?

Yes. Tokens protect against CSRF if properly implemented, but SameSite is a browser-enforced backup. If your token validation has a bug, SameSite still prevents the attack. Use both.

If I have CSP strict enough to block all inline scripts, do I still need CSRF tokens?

Yes. CSP prevents XSS (script injection), not CSRF (state-changing requests). An attacker cannot inject a script to steal your CSRF token, but they could still forge a request if tokens are missing.

Can an attacker bypass all three defenses?

Very difficult. Bypassing all three would require: stealing a valid CSRF token (difficult without XSS), stealing a session cookie (prevented by HttpOnly and SameSite), and bypassing CORS (requires server misconfiguration). A properly configured stack is resilient.

Should I monitor CSP violations in production?

Yes. CSP violations indicate either legitimate misconfigurations (resources being blocked) or potential attacks. Monitor them, adjust your policy, and alert on suspicious patterns (e.g., repeated blocks from the same attacker domain).

Further Reading