What is CSRF and how to prevent it?
CSRF (Cross-Site Request Forgery) is a web security vulnerability that allows attackers to trick users into performing unwanted actions on a website where they're authenticated.
How CSRF Works
Attack Scenario
- User logs into
bank.com(gets session cookie) - User visits malicious site
evil.com(while still logged in to bank.com) evil.comsends forged request tobank.com- Browser automatically includes bank.com cookies
- Bank processes request as if user initiated it
Example Attack
Vulnerable Bank Transfer:
<!-- On evil.com -->
<img src="https://bank.com/transfer?to=attacker&amount=1000" />
<!-- or -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="1000" />
</form>
<script>
document.forms[0].submit();
</script>When user loads this page, the request is automatically sent with their bank.com cookies!
Prevention Methods
1. CSRF Tokens (Most Common)
Generate unique token for each session/request:
Backend (Node.js/Express):
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
// Generate CSRF token
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/transfer', csrfProtection, (req, res) => {
// Token automatically validated
if (valid) {
processTransfer(req.body);
}
});Frontend:
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{ csrfToken }}" />
<input name="to" />
<input name="amount" />
<button>Transfer</button>
</form>With React/Fetch:
function Transfer() {
const [csrfToken, setCsrfToken] = useState('');
useEffect(() => {
fetch('/api/csrf-token')
.then(r => r.json())
.then(data => setCsrfToken(data.token));
}, []);
const handleTransfer = async () => {
await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken // Custom header
},
body: JSON.stringify({ to, amount })
});
};
return <button onClick={handleTransfer}>Transfer</button>;
}2. SameSite Cookies
Prevent browser from sending cookies with cross-site requests:
// Node.js/Express
res.cookie('sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict' // or 'lax'
});SameSite Values:
strict: Cookie never sent on cross-site requestslax: Cookie sent on top-level navigations (GET only)none: Cookie sent on all requests (requiressecure)
3. Custom Request Headers
Require custom headers that can't be set by forms:
// Frontend
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
// Backend
app.post('/api/transfer', (req, res) => {
if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
return res.status(403).send('Forbidden');
}
// Process transfer
});4. Verify Origin/Referer Headers
app.post('/api/transfer', (req, res) => {
const origin = req.headers.origin || req.headers.referer;
if (!origin || !origin.startsWith('https://bank.com')) {
return res.status(403).send('Invalid origin');
}
// Process transfer
});5. Re-authentication for Sensitive Actions
app.post('/api/delete-account', async (req, res) => {
// Require password confirmation
const user = await User.findById(req.userId);
if (!await user.checkPassword(req.body.password)) {
return res.status(401).send('Invalid password');
}
// Delete account
});Real-World Example: Complete Protection
// Express.js with multiple protections
const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const app = express();
// 1. SameSite cookies
app.use(cookieParser());
app.use((req, res, next) => {
res.cookie('session', req.sessionID, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
next();
});
// 2. CSRF tokens
const csrfProtection = csrf({ cookie: true });
// 3. Origin verification
const verifyOrigin = (req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://myapp.com'];
if (origin && !allowedOrigins.includes(origin)) {
return res.status(403).send('Forbidden');
}
next();
};
// Protected route with multiple defenses
app.post('/api/transfer',
verifyOrigin,
csrfProtection,
(req, res) => {
// Process transfer
}
);Testing for CSRF Vulnerabilities
<!-- Test page to check if site is vulnerable -->
<!DOCTYPE html>
<html>
<body>
<h1>CSRF Test</h1>
<form id="testForm" action="https://target-site.com/api/action" method="POST">
<input type="hidden" name="data" value="malicious" />
</form>
<script>
// Auto-submit when page loads
document.getElementById('testForm').submit();
</script>
</body>
</html>If action executes without additional verification, site is vulnerable!
Common Interview Questions
-
Q: What's the difference between CSRF and XSS? A: CSRF exploits user's authentication to perform actions. XSS injects malicious scripts. CSRF uses user as "confused deputy", XSS directly attacks user.
-
Q: Why can't attackers read CSRF token from the page? A: Due to Same-Origin Policy, JavaScript from evil.com cannot read content from bank.com.
-
Q: Is HTTPS enough to prevent CSRF? A: No! HTTPS prevents MITM attacks but doesn't stop CSRF. You still need CSRF tokens or other protections.
-
Q: Can GET requests have CSRF vulnerabilities? A: Yes! If GET requests change state (delete, transfer, etc.), they're vulnerable. Always use POST/PUT/DELETE for state changes.
Best Practices
✅ DO:
- Use CSRF tokens for state-changing operations
- Set SameSite=Strict for session cookies
- Verify Origin/Referer headers
- Use POST/PUT/DELETE for state changes (never GET)
- Require re-authentication for sensitive actions
❌ DON'T:
- Rely on HTTPS alone
- Use GET for state-changing operations
- Trust cookies alone for authentication
- Forget to validate tokens on every request
- Store CSRF tokens in cookies (use hidden fields or custom headers)
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.