CSRF (Cross-Site Request Forgery): Protecting User Actions and Building Trust
Imagine a user logged into their bank account in one browser tab while browsing social media in another. They click what appears to be a harmless link—a funny video, a news article—and unknowingly trigger a money transfer from their bank account. The bank's website processes the request because it came from the user's authenticated session. This is Cross-Site Request Forgery (CSRF), and it's one of the most dangerous vulnerabilities in web security because it exploits the trust relationship between users and interfaces.
CSRF attacks succeed not through technical complexity, but through interface deception. They trick browsers into making legitimate-looking requests that users never intended to make. Unlike other attacks that steal data, CSRF attacks perform unwanted actions using the user's own authenticated session—changing passwords, transferring money, modifying account settings, or posting content.
For design and development teams, CSRF protection isn't just about adding tokens to forms—it's about designing interfaces that naturally resist manipulation while maintaining seamless user experience. This guide covers CSRF fundamentals through the lens of user-centered design, showing how to build secure interfaces that users can trust.
What is CSRF? Understanding the Attack from a User Perspective
A Cross-Site Request Forgery (CSRF) is a web security vulnerability where attackers trick users' browsers into making unintended HTTP requests to sites where users are authenticated. Here's how it works:
The core of CSRF exploits a fundamental browser behavior: browsers automatically include authentication credentials (cookies, session tokens) with every request to a domain, regardless of where the request originates. Users don't see or approve the malicious request—the attack is completely invisible to them. When your bank's website processes a request, it appears legitimate because it includes valid authentication credentials.
The vulnerability is called "forgery" because requests appear legitimate to the target site—they include valid authentication, come from the user's browser, and often look identical to genuine requests the user would make themselves. But the user never initiated them.
How CSRF Differs from Other Attacks
CSRF is often confused with other web vulnerabilities, but it has distinct characteristics that make it uniquely dangerous:
CSRF vs XSS (Cross-Site Scripting)
- CSRF performs actions using the user's existing credentials—the attacker cannot see response data
- XSS executes malicious scripts in the user's browser context—the attacker can see sensitive information
- Key difference: XSS gives attackers read access; CSRF enables unwanted write access
CSRF vs Clickjacking
- CSRF is completely invisible—users don't see or approve the malicious request
- Clickjacking tricks users into visibly clicking hidden elements—they participate unknowingly
- Key difference: Clickjacking requires user interaction; CSRF doesn't
CSRF vs Phishing
- Phishing steals user credentials through deception (fake login forms, emails)
- CSRF uses existing credentials the attacker never possesses
- Key difference: Phishing targets credentials; CSRF exploits active sessions
The unique danger of CSRF: the attacker cannot see response data or directly steal information, but they can execute any state-changing action the user has permission to perform. This means an attacker can delete accounts, transfer funds, change permissions, or publish content—all without ever knowing the user's password.
Real-World CSRF Attack Scenarios
Scenario 1: Social Media Account Compromise
A user browses a forum while logged into Twitter in another tab. The forum contains a malicious image tag: ``. When the page loads, the browser automatically requests the image URL—including the user's Twitter session cookie. Before the user realizes anything happened, their email address has changed, and the attacker has gained account control.
Scenario 2: Banking Transaction
A user checks their bank balance while browsing a news website. Embedded on the news page is malicious JavaScript that submits a hidden form:
The JavaScript automatically submits this form. The bank receives a legitimate-looking request from an authenticated session and processes the transfer. The attacker's account receives the funds while the user is still browsing the news site.
Scenario 3: Admin Panel Manipulation
A website administrator visits a compromised website while logged into their CMS. The malicious site contains a hidden iframe that POSTs to cms.com/users/create-admin with attacker credentials. The CMS processes the request as if the administrator initiated it, creating a new admin account for the attacker.
These scenarios illustrate a critical point: CSRF attacks don't require the attacker to be sophisticated—they just need to understand how your application works and exploit the browser's automatic credential inclusion.
Cross-Domain Requests: The Foundation of CSRF
To understand why CSRF protection is necessary, you need to understand how browsers handle cross-origin requests and cookie behavior. This is the fundamental mechanism that enables CSRF attacks.
The Browser's Same-Origin Policy
Browsers implement a Same-Origin Policy that restricts how documents or scripts from one origin can interact with resources from another origin. An origin is defined by the combination of protocol (http/https), domain, and port. This policy prevents a script on evil.com from directly accessing data on your-bank.com.
However, the Same-Origin Policy has an important exception: cookies are automatically included with requests to their domain, even when the request originates from a different site. This is why you can open multiple tabs, follow links, and stay logged in everywhere. Your browser manages your sessions seamlessly across tabs and windows.
But this automatic cookie inclusion creates CSRF vulnerability. When you visit malicious-site.com, and that page includes a form or script that makes a request to your-bank.com, your browser:
- Sees the request is going to
your-bank.com - Checks for cookies associated with
your-bank.com - Automatically attaches those cookies to the request
- Sends the fully-authenticated request
- The receiving server has no way to know the request wasn't initiated by the user
From the user's perspective, this behavior makes web browsing seamless. But it means any website can attempt to make requests to any other website using your credentials.
Why CORS Doesn't Prevent CSRF
A common misconception is that Cross-Origin Resource Sharing (CORS) protects against CSRF. It doesn't.
CORS restricts reading responses from cross-origin requests, but it doesn't prevent the request from being sent and processed. For state-changing operations (POST, PUT, DELETE), the damage is done the moment the server processes the request—the attacker doesn't need to read the response.
Additionally, simple requests using POST with application/x-www-form-urlencoded content type don't trigger CORS preflight checks, so they execute immediately. A malicious form submission travels from evil.com to your server without any CORS preflight protection.
Complex requests with custom headers and JSON content do trigger preflight checks, but relying only on CORS for CSRF protection is insufficient. You need explicit anti-CSRF measures like tokens or SameSite cookies.
The Three Conditions for Successful CSRF Attacks
According to security research, three conditions must exist for CSRF attacks to succeed. Understanding these conditions helps developers identify where protection is needed.
1. A Relevant Action Worth Attacking
The target application must have a privileged or state-changing action that's valuable to attack. Examples include:
- Changing user email or password
- Transferring money or making purchases
- Modifying account permissions or settings
- Publishing content or sending messages
- Deleting data or closing accounts
- Creating user accounts with elevated privileges
These are exactly the actions users perform regularly, so CSRF protection must be seamless. Any friction in legitimate workflows reduces usability and frustrates users.
2. Cookie-Based Session Handling
The application must use cookies (or HTTP authentication) to track user sessions. This is the critical vulnerability: browsers automatically include cookies with every request to the cookie's domain, regardless of request origin.
Alternative authentication methods are less vulnerable:
- Bearer tokens in Authorization headers - Must be explicitly added by JavaScript; not automatically sent
- Custom headers - Cannot be set by simple HTML forms; require JavaScript and trigger CORS preflight
- Local Storage - Values not automatically sent with requests
This creates an interesting trade-off: cookie-based sessions offer excellent user experience (seamless, automatic) but require explicit CSRF protection. Token-based authentication requires more JavaScript handling but has inherent CSRF resistance because tokens must be explicitly included in requests.
3. No Unpredictable Request Parameters
The attacker must be able to determine or guess all parameters required for the malicious request. A vulnerable endpoint might look like:
POST /email/change HTTP/1.1
Host: your-app.com
Content-Type: application/x-www-form-urlencoded
[email protected]
An attacker can easily forge this request because all parameters are predictable. A protected endpoint requires an unpredictable parameter:
POST /email/change HTTP/1.1
Host: your-app.com
Content-Type: application/x-www-form-urlencoded
[email protected]&csrf_token=9f8e7d6c5b4a3210
The unpredictable parameter (CSRF token) must be easy for legitimate users to obtain and submit, but impossible for attackers to guess.
How CSRF Attacks Work: Common Delivery Methods
Attackers use several techniques to deliver CSRF attacks. Understanding these methods helps you recognize where protection is needed.
Hidden Form Auto-Submission
The classic CSRF attack delivers a hidden form that automatically submits:
Click here to see funny cat video!
// Auto-submit form when page loads
document.getElementById('csrf-form').submit();
When a user visits this page, the sequence is:
- User clicks link to
evil.com(via email, social media, forum) - Page loads and immediately submits the hidden form
- Browser sends POST to
your-bank.comincluding user's session cookie - Bank processes legitimate-looking authenticated request
- Money transfers to attacker's account
- User experiences brief page load or redirect but attack completes invisibly
Image Tag Exploitation (GET-Based Attacks)
GET-based attacks work by exploiting the browser's automatic image requests:
The browser automatically requests the image source, including cookies for vulnerable-site.com. The user's account is deleted without any visible action.
Important design lesson: Never use GET requests for state-changing operations. GET should be safe and idempotent (producing the same result no matter how many times it's called).
AJAX/Fetch API Attacks
Modern applications using AJAX are vulnerable if improperly configured:
// Malicious script on evil.com
fetch('https://api.target-site.com/user/settings', {
method: 'PUT',
credentials: 'include', // Send cookies
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: '[email protected]',
notify_password_changes: false
})
})
If the server has misconfigured CORS, this request succeeds. Even with CORS preflight checks, if the server allows cross-origin requests, the attack completes.
Iframe-Based Attacks
Silent execution through hidden iframes:
An administrator visiting a compromised website while logged into their admin panel unknowingly loads the iframe, which silently creates a new admin account for the attacker.
CSRF Protection Methods: From Tokens to Modern Defenses
Effective CSRF protection requires a layered approach, combining multiple techniques and prioritizing user experience.
1. Synchronizer Token Pattern (Anti-CSRF Tokens)
The most common and effective method uses unpredictable, unique tokens. Here's how it works:
- Server generates a unique, unpredictable token for the user's session
- Token is embedded in forms or available to JavaScript
- User submits form or AJAX request including the token
- Server validates the token matches the session
- Request is processed only if token is valid
Implementation Example:
Transfer
UX Advantages:
- Completely transparent to users
- No additional user actions required
- Works with standard HTML forms
- Compatible with all browsers
UX Considerations:
- Token expiration can cause form submission failures
- Back button may show expired token
- Multiple tabs can have token sync issues
Best Practices for Good UX:
Use session-based tokens rather than per-request tokens. Per-request tokens break back button functionality and create confusion for users. Session tokens remain valid throughout the session, providing good security without UX friction.
Implement graceful expiration handling with automatic token refresh:
fetch('/api/endpoint', {
method: 'POST',
headers: {
'X-CSRF-Token': getCsrfToken()
},
body: formData
})
.catch(error => {
if (error.status === 403) { // CSRF token invalid
// Refresh token and retry
refreshCsrfToken().then(() => retryRequest())
}
})
Consider auto-token refresh that refreshes the token before expiration, handling this transparently so users never see token expiry errors.
2. SameSite Cookie Attribute
Modern browsers support the SameSite cookie attribute, which restricts when browsers send cookies with cross-site requests. This provides native protection at the browser level.
The attribute has three modes:
| Mode | Behavior | When to Use | UX Impact |
|---|---|---|---|
Strict | Cookie never sent with cross-site requests | Maximum security for sensitive sites | Breaks some legitimate cross-site flows (email links, external redirects) |
Lax | Cookie sent with top-level navigation GET requests | Good balance for most sites | Minimal UX impact |
None | Cookie sent with all requests (requires Secure flag) | Third-party integrations, legacy apps | No CSRF protection |
Implementation Example:
Set-Cookie: session=abc123; SameSite=Lax; Secure; HttpOnly
Understanding the difference with an email scenario:
With SameSite=Strict:
- User receives password reset email
- Clicks link to
yoursite.com/reset?token=xyz - Browser navigates to site (cross-site GET request)
- Session cookie NOT sent (Strict prevents it)
- User appears logged out, must log in again
With SameSite=Lax:
- User receives password reset email
- Clicks link to
yoursite.com/reset?token=xyz - Browser navigates to site (cross-site GET request)
- Session cookie IS sent (Lax allows top-level navigation)
- User remains logged in, seamless experience
Best Practice: Use SameSite=Lax for session cookies, combined with CSRF tokens for maximum security with good UX.
3. Custom Request Headers (AJAX/API Protection)
This approach is excellent for modern SPAs and APIs. Add a custom header to AJAX requests—browsers prevent cross-origin requests from adding custom headers without CORS preflight, providing natural CSRF protection.
Implementation Example:
// Client-side (React, Vue, Angular)
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
// Or with fetch
fetch('/api/endpoint', {
method: 'POST',
headers: {
'X-CSRF-Token': getCsrfToken(),
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
Server-Side Validation:
// Express middleware
function validateCsrfHeader(req, res, next) {
const token = req.headers['x-csrf-token']
if (!token || token !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' })
}
next()
}
UX Advantages:
- Perfect for single-page applications
- No form modifications needed
- Handles AJAX requests naturally
- Works well with REST APIs
Token Storage Options:
- Meta Tag Approach (Simple):
const token = document.querySelector('meta[name="csrf-token"]').content
- API Endpoint (Dynamic):
async function getCsrfToken() {
const response = await fetch('/api/csrf-token')
const data = await response.json()
return data.token
}
- Cookie-to-Header Pattern (Double Submit):
- Server sets CSRF token in cookie
- Client reads cookie value and sends as header
- Server validates cookie matches header
4. Double Submit Cookie Pattern
This stateless approach provides CSRF protection without server-side session storage:
- Server sets random token in cookie
- Client reads cookie value and submits in request parameter or header
- Server validates cookie matches submitted value
- No server-side session storage needed
Implementation Example:
// Server sets cookie
res.cookie('csrf_token', generateRandomToken(), {
httpOnly: false, // Must be readable by JavaScript
sameSite: 'Strict',
secure: true
})
// Client submits token
fetch('/api/endpoint', {
method: 'POST',
headers: {
'X-CSRF-Token': getCookie('csrf_token')
},
body: data
})
// Server validates
function validateDoubleSubmit(req, res, next) {
const cookieToken = req.cookies.csrf_token
const headerToken = req.headers['x-csrf-token']
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' })
}
next()
}
UX Advantages:
- No server-side session state required
- Scales well for distributed systems
- Transparent to users
- Works with stateless architectures
UX Considerations:
- Cookie must NOT be HttpOnly (JavaScript needs to read it)
- Vulnerable if subdomain is compromised (can set cookies)
- Requires HTTPS (secure cookie flag)
Enhanced Security: You can sign tokens using HMAC to prevent tampering:
// Server creates signed token
const token = crypto.randomBytes(32).toString('hex')
const signature = hmac(token, serverSecret)
const signedToken = `${token}.${signature}`
// Server verifies signature before comparing
5. Origin and Referer Header Validation
This defense-in-depth layer checks Origin or Referer headers to verify requests come from your domain.
Implementation Example:
function validateOrigin(req, res, next) {
const origin = req.headers.origin || req.headers.referer
const allowedOrigins = ['https://yoursite.com', 'https://www.yoursite.com']
if (!origin || !allowedOrigins.some(allowed => origin.startsWith(allowed))) {
return res.status(403).json({ error: 'Invalid origin' })
}
next()
}
UX Advantages:
- Completely transparent
- No client-side changes needed
- Works with all request types
UX Considerations:
- Some proxies/firewalls strip headers
- Browser privacy settings may omit headers
- Not reliable as sole protection method
Best Practice: Use as an additional layer, not primary defense. Combine with tokens or SameSite cookies.
6. User Interaction-Based Protection
For high-sensitivity actions, require explicit user actions that confirm intent:
Re-Authentication:
This action cannot be undone. Please confirm your password:
Delete Account
CAPTCHA Challenges:
- Google reCAPTCHA
- hCaptcha
- Cloudflare Turnstile
Confirmation Dialogs:
// Two-step confirmation for critical actions
function deleteAccount() {
if (confirm('Are you sure you want to delete your account?')) {
if (confirm('This action is permanent. Continue?')) {
submitDeleteRequest()
}
}
}
UX Considerations:
- ✅ Very high security for critical operations
- ✅ Clear user intent verification
- ✅ Provides peace of mind for destructive actions
- ⚠️ Adds friction to user workflows
- ⚠️ Can frustrate users if overused
- ⚠️ Accessibility concerns with some CAPTCHA types
When to Use:
- Account deletion
- Large financial transactions
- Permission/role changes
- Data export or bulk operations
- Security setting modifications
When NOT to Use:
- Regular form submissions
- Routine data updates
- Frequent actions (posting comments, liking content)
- Low-risk operations
Risk-Based Protection:
// Adjust protection level based on action risk
if (transaction.amount > 10000) {
requireReAuthentication()
} else if (transaction.amount > 1000) {
requireCaptcha()
} else {
// Standard CSRF token only
}
Implementation Best Practices: Building Secure, Usable Interfaces
Framework-Specific Implementation
Different frameworks provide different levels of CSRF protection out of the box.
Express.js (Node.js) with csurf middleware:
const csrf = require('csurf')
const cookieParser = require('cookie-parser')
const csrfProtection = csrf({ cookie: true })
app.use(cookieParser())
// Render form with token
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() })
})
// Validate on submission
app.post('/submit', csrfProtection, (req, res) => {
res.json({ message: 'Form processed successfully' })
})
Django (Python) has built-in CSRF middleware enabled by default:
# settings.py
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
# ...
]
# Template
{% csrf_token %}
Laravel (PHP) automatically generates tokens:
@csrf
React/Next.js requires custom setup:
// pages/api/csrf-token.ts
const token = generateCsrfToken(req.session)
res.status(200).json({ csrfToken: token })
}
// Component
function MyForm() {
const [csrfToken, setCsrfToken] = useState('')
useEffect(() => {
fetch('/api/csrf-token')
.then(res => res.json())
.then(data => setCsrfToken(data.csrfToken))
}, [])
const handleSubmit = async (e) => {
e.preventDefault()
await fetch('/api/submit', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
}
return ...
}
Token Management Best Practices
Security Requirements for Token Generation:
Use cryptographically secure random tokens:
// Good: Cryptographically secure random token
const crypto = require('crypto')
const token = crypto.randomBytes(32).toString('hex')
// Bad: Predictable token
const token = Math.random().toString(36)
Minimum token length: 128 bits (16 bytes). Recommended: 256 bits (32 bytes).
Server-Side Token Storage Options:
-
Session Storage (Most Common):
- Secure: ✅
- Per-user tokens: ✅
- Requires session infrastructure: ⚠️
-
Signed Cookies:
- Stateless: ✅
- Scales horizontally: ✅
- Cookie must be readable by JS: ⚠️
-
Database Storage (Enterprise):
- Persistent across server restarts: ✅
- Audit trail: ✅
- Performance overhead: ⚠️
Token Expiration Strategy:
Session-based tokens (recommended for UX):
// Token valid for entire session
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateToken()
}
next()
})
Time-based tokens (higher security):
// Token expires after 1 hour
const TOKEN_LIFETIME = 60 * 60 * 1000 // 1 hour
app.use((req, res, next) => {
const now = Date.now()
if (!req.session.csrfToken ||
(now - req.session.csrfTokenCreated) > TOKEN_LIFETIME) {
req.session.csrfToken = generateToken()
req.session.csrfTokenCreated = now
}
next()
})
Token Rotation:
Rotate tokens after login (new session, new token), after privilege escalation, and after password change. However, do NOT rotate on every request—this breaks back button functionality and creates UX issues.
// Rotate after login
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body)
// Create new session
req.session.regenerate((err) => {
req.session.userId = user.id
req.session.csrfToken = generateToken() // Fresh token
res.json({ success: true })
})
})
Error Handling and User Communication
Graceful Failure Responses:
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
// CSRF token validation failed
if (req.xhr || req.headers.accept?.includes('application/json')) {
// AJAX request - return JSON
return res.status(403).json({
error: 'Session expired. Please refresh the page.',
code: 'CSRF_TOKEN_INVALID'
})
} else {
// Traditional form - redirect with message
req.flash('error', 'Your session expired. Please try again.')
return res.redirect(req.originalUrl)
}
}
next(err)
})
Client-Side Error Handling:
async function submitForm(data) {
try {
const response = await fetch('/api/submit', {
method: 'POST',
headers: {
'X-CSRF-Token': getCsrfToken(),
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
if (!response.ok) {
const error = await response.json()
if (error.code === 'CSRF_TOKEN_INVALID') {
// Attempt automatic recovery
await refreshCsrfToken()
// Retry request with new token
return submitForm(data)
}
throw new Error(error.message)
}
return response.json()
} catch (error) {
showUserFriendlyError('Something went wrong. Please try again.')
}
}
User-Friendly Error Messages:
❌ Bad: "CSRF token validation failed" ✅ Good: "Your session expired. Please refresh the page and try again."
❌ Bad: "403 Forbidden" ✅ Good: "For your security, this action couldn't be completed. Please try again."
Testing CSRF Protection
Manual Testing:
Attempt requests without tokens:
curl -X POST https://yoursite.com/api/submit \
-H "Cookie: session=abc123" \
-H "Content-Type: application/json" \
-d '{"data":"test"}'
# Expected: 403 Forbidden
Attempt requests with valid tokens:
curl -X POST https://yoursite.com/api/submit \
-H "Cookie: session=abc123" \
-H "X-CSRF-Token: valid-token-here" \
-H "Content-Type: application/json" \
-d '{"data":"test"}'
# Expected: 200 OK
Automated Testing:
describe('CSRF Protection', () => {
it('should reject requests without CSRF token', async () => {
const response = await request(app)
.post('/api/submit')
.send({ data: 'test' })
expect(response.status).toBe(403)
})
it('should accept requests with valid CSRF token', async () => {
const session = await createSession()
const token = session.csrfToken
const response = await request(app)
.post('/api/submit')
.set('X-CSRF-Token', token)
.set('Cookie', `session=${session.id}`)
.send({ data: 'test' })
expect(response.status).toBe(200)
})
it('should reject requests with invalid CSRF token', async () => {
const response = await request(app)
.post('/api/submit')
.set('X-CSRF-Token', 'invalid-token')
.send({ data: 'test' })
expect(response.status).toBe(403)
})
})
Common CSRF Vulnerabilities and How to Avoid Them
Token Validation Only on POST: Never limit CSRF protection to POST requests. Validate all state-changing operations (POST, PUT, DELETE, PATCH). Ensure GET requests never modify state.
Flawed Token Validation: Always require tokens to be present AND valid. Don't accept requests with missing tokens:
// ❌ Bad: Accepts request if token is missing
if (req.body.csrf_token && req.body.csrf_token !== req.session.csrf) {
return res.status(403).send('Invalid token')
}
// ✅ Good: Requires token to be present AND valid
if (!req.body.csrf_token || req.body.csrf_token !== req.session.csrf) {
return res.status(403).send('Invalid token')
}
Missing SameSite Attribute: Always set SameSite on session cookies:
res.cookie('session', sessionId, {
sameSite: 'Lax', // or 'Strict'
secure: true,
httpOnly: true
})
Subdomain Token Leakage: Don't set cookie domain attributes to parent domains. Restrict cookies to specific hosts:
// ❌ Bad: Accessible to all subdomains
res.cookie('session', sessionId, {
domain: '.example.com'
})
// ✅ Good: Defaults to current host only
res.cookie('session', sessionId, {
sameSite: 'Strict',
secure: true,
httpOnly: true
})
Token Tied to Wrong Scope: Tokens must be user-specific and session-specific, never global:
// ❌ Bad: Same token for everyone
const GLOBAL_CSRF_TOKEN = generateToken()
// ✅ Good: Unique per session
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateToken()
}
next()
})
CSRF Protection in Modern Web Development
Single-Page Applications (SPAs)
SPAs have unique CSRF considerations due to long-lived sessions and heavy AJAX usage.
Token Delivery via API:
// React example with hooks
function useCsrfToken() {
const [token, setToken] = useState('')
useEffect(() => {
async function fetchToken() {
const response = await fetch('/api/csrf-token')
const data = await response.json()
setToken(data.csrfToken)
}
fetchToken()
}, [])
return token
}
// Use in components
function MyForm() {
const csrfToken = useCsrfToken()
const handleSubmit = async (data) => {
await fetch('/api/submit', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
}
return ...
}
Axios Interceptor Pattern (applies to all requests):
axios.interceptors.request.use(config => {
const token = getCsrfToken()
config.headers['X-CSRF-Token'] = token
return config
})
// Automatic token refresh on 403
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 403 &&
error.response?.data?.code === 'CSRF_TOKEN_INVALID') {
await refreshCsrfToken()
const newToken = getCsrfToken()
error.config.headers['X-CSRF-Token'] = newToken
return axios.request(error.config)
}
return Promise.reject(error)
}
)
RESTful APIs
Token Delivery Endpoint:
app.get('/api/csrf-token', (req, res) => {
const token = req.session.csrfToken || generateToken()
req.session.csrfToken = token
res.json({ csrfToken: token })
})
Header Validation Middleware:
app.use('/api/*', (req, res, next) => {
// Allow GET/HEAD/OPTIONS without token
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next()
}
const token = req.headers['x-csrf-token']
if (!token || token !== req.session.csrfToken) {
return res.status(403).json({
error: 'CSRF token validation failed',
code: 'CSRF_TOKEN_INVALID'
})
}
next()
})
GraphQL
Mutation Protection via Apollo Server context:
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req, res }) => {
// Validate CSRF for mutations
const isMutation = req.body.query?.trim().startsWith('mutation')
if (isMutation) {
const token = req.headers['x-csrf-token']
if (!token || token !== req.session.csrfToken) {
throw new ForbiddenError('Invalid CSRF token')
}
}
return { req, res }
}
})
Client Setup with Apollo Client:
const httpLink = createHttpLink({
uri: '/graphql',
credentials: 'include'
})
const csrfLink = setContext((_, { headers }) => {
const token = getCsrfToken()
return {
headers: {
...headers,
'x-csrf-token': token
}
}
})
const client = new ApolloClient({
link: csrfLink.concat(httpLink),
cache: new InMemoryCache()
})
User Experience Considerations
Balancing Security with Usability
The most secure CSRF protection is useless if it frustrates users into abandoning your application. Different protection strategies offer different security-to-usability trade-offs:
| Approach | Security | Usability | Best For |
|---|---|---|---|
| No protection | ❌ Very Low | ✅ Perfect | Never—don't do this |
| SameSite cookies only | ⚠️ Moderate | ✅ Excellent | Low-risk applications |
| Session-based tokens | ✅ High | ✅ Good | Most applications |
| Per-request tokens | ✅ Very High | ⚠️ Poor (breaks back button) | Not recommended |
| Re-authentication | ✅ Maximum | ⚠️ Moderate friction | High-risk operations only |
Common UX Problems and Solutions
Problem: Token Expiry During Long Forms
Scenario: User fills out lengthy form over 30 minutes. Session expires. Form submission fails.
Solutions:
-
Auto-Save Drafts: Save form data to localStorage periodically, allowing recovery if submission fails
-
Session Extension on Activity: Extend session when user actively fills the form
-
Refresh Token Before Submit: Check if token is close to expiry before submitting
Problem: Back Button with Expired Token
Scenario: User submits form, clicks back button, modifies data, resubmits. Token expired.
Solutions:
-
Use Session-Based Tokens: Tokens valid for entire session survive back button navigation
-
Client-Side Token Validation: Refresh token when back button used (pageshow event)
-
Warning on Navigation: Warn users they may lose form data
Problem: Multiple Tabs/Windows
Scenario: User has application open in multiple tabs. Token refresh in one tab doesn't propagate to others.
Solutions:
- BroadcastChannel for Tab Sync: When token refreshes, notify other tabs
- SharedWorker for Token Management: Centralize token management across tabs
- LocalStorage Events: Update token in localStorage, listen for changes from other tabs
Implementing CSRF Protection: A Practical Roadmap
Ready to secure your application? Follow this implementation roadmap:
1. Audit Current Protection
- Check if CSRF protection exists
- Review which endpoints are protected
- Test protection effectiveness
- Identify gaps in coverage
2. Choose Protection Strategy
- Most Applications: Synchronizer token pattern + SameSite cookies
- SPAs/APIs: Custom headers + token-based auth
- Microservices: API gateway with centralized CSRF validation
- High-Security: Multi-factor with re-authentication for critical actions
3. Implement Core Protection
- Add CSRF middleware/framework support
- Configure SameSite cookie attributes
- Implement token generation and validation
- Update forms and AJAX requests
- Add error handling and recovery
4. Test Thoroughly
- Unit tests for token validation
- Integration tests for workflows
- Manual cross-origin testing
- Security scanning (OWASP ZAP, Burp Suite)
- UX testing (ensure no friction)
5. Monitor and Maintain
- Set up logging for CSRF failures
- Create monitoring dashboards
- Configure alerts for attack patterns
- Plan incident response procedures
Next Steps
CSRF protection is critical for user trust and security. When implemented thoughtfully, it's invisible to users while providing robust protection against attacks that could compromise their accounts, data, and finances.
The key to successful CSRF protection is balancing security with usability—protection that users never notice but appreciate knowing exists.
Need expert help? Contact Digital Thrive to discuss securing your application with effective CSRF protection that maintains seamless user experience. Our team builds secure, user-friendly interfaces with proper CSRF protection baked in from the start, combining security-first design with proven implementation patterns across all major platforms.
Related resources:
- Web Development Services - Secure applications built right
- Web Design Services - Interfaces designed with security in mind
- Analytics Services - Monitor security events and user behavior
- Core Web Vitals Optimization - Performance that doesn't sacrifice security
- Mobile Usability Guide - Secure mobile experiences
- [React useState Guide](/guides/web development/react-usestate) - State management patterns
- [React useContext Guide](/guides/web development/react-usecontext) - Context for security tokens
- Schema Markup Implementation - Structured data for trusted sites
Sources
- MDN Web Docs - Cross-site request forgery (CSRF)
- OWASP Foundation - Cross Site Request Forgery
- PortSwigger Web Security Academy - CSRF Tutorial
- OWASP - Cross-Site Request Forgery Prevention Cheat Sheet
- Microsoft Learn - Prevent CSRF attacks in ASP.NET Core
- Acunetix - What is CSRF?
- Bright Security - 6 CSRF Protection Best Practices
- Bright Security - What is a CSRF Token?