Cross-Site Request Forgery (CSRF) remains a significant threat to web applications. Here’s how to comprehensively protect your React applications against CSRF attacks:
Core Defense Mechanisms
1. Anti-CSRF Tokens (Synchronizer Token Pattern)
Server-Side Implementation (Node.js/Express Example):
// Generate and store CSRF token
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}
next();
});
// Verify CSRF token middleware
const verifyCsrf = (req, res, next) => {
const clientToken = req.headers['x-csrf-token'] || req.body.csrfToken;
if (!clientToken || clientToken !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
};
React Integration:
// Custom hook to fetch and manage CSRF token
const useCsrfToken = () => {
const [csrfToken, setCsrfToken] = useState('');
useEffect(() => {
const fetchCsrfToken = async () => {
const response = await fetch('/api/csrf-token', {
credentials: 'include' // Important for session cookies
});
const data = await response.json();
setCsrfToken(data.token);
};
fetchCsrfToken();
}, []);
return csrfToken;
};
// Usage in component
const FormComponent = () => {
const csrfToken = useCsrfToken();
const handleSubmit = async (e) => {
e.preventDefault();
await fetch('/api/sensitive-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ data: 'value' }),
credentials: 'include'
});
};
return (
<form onSubmit={handleSubmit}>
<input type="hidden" name="csrfToken" value={csrfToken} />
{/* Other form fields */}
</form>
);
};
Advanced Protection Strategies
2. SameSite Cookie Attribute
Express.js Configuration:
app.use(session({
secret: 'your-secret-key',
cookie: {
sameSite: 'strict', // or 'lax' for GET requests from external sites
secure: true, // HTTPS only
httpOnly: true
}
}));
3. Double Submit Cookie Pattern
Implementation:
// Server middleware to set cookie and verify
app.use((req, res, next) => {
const cookieToken = req.cookies['X-CSRF-TOKEN'];
const headerToken = req.headers['x-csrf-token'];
if (req.method !== 'GET' && cookieToken !== headerToken) {
return res.status(403).send('CSRF validation failed');
}
if (!cookieToken) {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('X-CSRF-TOKEN', token, {
sameSite: 'strict',
secure: true
});
}
next();
});
// React - automatically include token from cookie in requests
axios.interceptors.request.use(config => {
const token = document.cookie
.split('; ')
.find(row => row.startsWith('X-CSRF-TOKEN='))
?.split('=')[1];
if (token && ['post', 'put', 'patch', 'delete'].includes(config.method)) {
config.headers['X-CSRF-Token'] = token;
}
return config;
});
Framework-Specific Solutions
1. Next.js with CSRF Protection
API Route (Next.js):
// pages/api/csrf.js
export default function handler(req, res) {
if (req.method === 'GET') {
const token = crypto.randomBytes(32).toString('hex');
res.setHeader('Set-Cookie', `csrfToken=${token}; HttpOnly; SameSite=Strict; Secure`);
return res.status(200).json({ token });
}
res.status(405).end();
}
// Custom _app.js to inject CSRF token
function MyApp({ Component, pageProps, csrfToken }) {
return (
<>
<Component {...pageProps} csrfToken={csrfToken} />
<script
dangerouslySetInnerHTML={{
__html: `window.__CSRF_TOKEN__ = "${csrfToken}";`
}}
/>
</>
);
}
MyApp.getInitialProps = async ({ ctx }) => {
const csrfResponse = await fetch(`${process.env.BASE_URL}/api/csrf`, {
headers: ctx.req ? { cookie: ctx.req.headers.cookie } : undefined
});
const { token } = await csrfResponse.json();
return { csrfToken: token };
};
Security Headers for CSRF Protection
1. Content Security Policy (CSP)
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
form-action 'self';
frame-ancestors 'none';">
2. Additional Security Headers
// Express.js configuration
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
formAction: ["'self'"], // Prevent form submission to external domains
frameAncestors: ["'none'"] // Prevent framing attacks
}
},
referrerPolicy: { policy: 'same-origin' }
}));
Real-World Implementation Patterns
1. Protected API Client
// apiClient.js
const apiClient = axios.create({
baseURL: process.env.API_BASE_URL,
withCredentials: true
});
// Add CSRF token to all mutating requests
apiClient.interceptors.request.use(config => {
if (['post', 'put', 'patch', 'delete'].includes(config.method)) {
const csrfToken = getCsrfToken(); // From cookie or state
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
// Handle CSRF token errors
apiClient.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 403 &&
error.response.data?.error === 'Invalid CSRF token') {
// Trigger token refresh and retry
return handleCsrfError(error);
}
return Promise.reject(error);
}
);
2. Stateless CSRF with JWT
Alternative Approach for JWT-based Apps:
// Generate CSRF token derived from JWT
const generateCsrfToken = (jwtToken) => {
const secret = jwtToken.split('.')[2]; // Use signature part
return crypto.createHmac('sha256', secret)
.update('csrf-token')
.digest('hex');
};
// Middleware to verify
const verifyCsrf = (req, res, next) => {
const jwtToken = req.cookies.jwt;
const csrfToken = req.headers['x-csrf-token'];
if (!jwtToken || !csrfToken || csrfToken !== generateCsrfToken(jwtToken)) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
};
Testing Your CSRF Protections
1. Manual Testing with cURL
# Should fail without CSRF token
curl -X POST https://yourapp.com/api/sensitive-action \
-H "Content-Type: application/json" \
-d '{"data":"value"}'
# Should succeed with CSRF token
curl -X POST https://yourapp.com/api/sensitive-action \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: valid-token" \
-b "sessionId=abc123; X-CSRF-TOKEN=valid-token" \
-d '{"data":"value"}'
2. Automated Security Tests
// Jest test example
describe('CSRF Protection', () => {
it('should reject POST requests without CSRF token', async () => {
const response = await request(app)
.post('/api/sensitive-action')
.send({ data: 'test' });
expect(response.status).toBe(403);
});
it('should accept POST requests with valid CSRF token', async () => {
const csrfResponse = await request(app).get('/api/csrf-token');
const { token } = csrfResponse.body;
const response = await request(app)
.post('/api/sensitive-action')
.set('X-CSRF-Token', token)
.set('Cookie', csrfResponse.headers['set-cookie'])
.send({ data: 'test' });
expect(response.status).toBe(200);
});
});
Best Practices Checklist
- [ ] Implement anti-CSRF tokens for state-changing requests
- [ ] Set
SameSite=Strict
orLax
for session cookies - [ ] Use secure, HttpOnly cookies for token storage
- [ ] Enforce CSRF protection on all mutating endpoints
- [ ] Implement proper error handling for CSRF failures
- [ ] Add CSP headers to prevent unauthorized form actions
- [ ] Regularly rotate CSRF tokens (per-session is ideal)
- [ ] Consider double-submit cookie pattern for SPAs
- [ ] Test CSRF protections manually and automatically
- [ ] Educate team about CSRF risks and protections
Ongoing Maintenance
- Monitoring: Log CSRF validation failures for security auditing
- Updates: Keep dependencies (especially security middleware) updated
- Review: Periodically reassess CSRF protections as application evolves
- Training: Conduct security workshops for new team members