CSRF Protection in JavaScript
Cross-Site Request Forgery (CSRF) is a critical vulnerability. This article covers CSRF attacks, prevention techniques, and secure implementation patterns.
Introduction
CSRF protection provides:
- Request validation
- Attack prevention
- User account security
- Data integrity
- Compliance
Understanding CSRF helps you:
- Identify vulnerabilities
- Implement defenses
- Secure forms
- Protect APIs
- Build secure applications
CSRF Attack Basics
Understanding CSRF
// โ Bad: Vulnerable to CSRF
// User is logged into bank.com
// User visits attacker.com which contains:
// <img src="https://bank.com/transfer?amount=1000&to=attacker">
// The request is sent with user's bank.com cookies!
// โ
Good: CSRF protection prevents this
// Server validates CSRF token before processing request
Attack Scenarios
// โ Bad: Vulnerable form
// <form action="https://bank.com/transfer" method="POST">
// <input name="amount" value="1000">
// <input name="to" value="attacker">
// </form>
// <script>document.forms[0].submit();</script>
// โ
Good: Protected form includes CSRF token
// <form action="https://bank.com/transfer" method="POST">
// <input name="amount" value="1000">
// <input name="to" value="attacker">
// <input name="csrf_token" value="unique_token_here">
// </form>
CSRF Protection Techniques
Token-based Protection
// โ
Good: CSRF token generation and validation
class CSRFTokenManager {
static generateToken() {
// Generate cryptographically secure random token
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
static storeToken(token) {
// Store in session (server-side)
// Or in meta tag (client-side)
const meta = document.createElement('meta');
meta.name = 'csrf-token';
meta.content = token;
document.head.appendChild(meta);
}
static getToken() {
return document.querySelector('meta[name="csrf-token"]')?.content;
}
static validateToken(token, sessionToken) {
// Constant-time comparison to prevent timing attacks
return this.constantTimeCompare(token, sessionToken);
}
static constantTimeCompare(a, b) {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
}
// Usage
const token = CSRFTokenManager.generateToken();
CSRFTokenManager.storeToken(token);
console.log(CSRFTokenManager.getToken()); // Retrieve token
Form Protection
// โ
Good: Protect forms with CSRF tokens
class SecureFormHandler {
constructor(formId) {
this.form = document.getElementById(formId);
this.setupCSRFToken();
this.setupEventListeners();
}
setupCSRFToken() {
// Add CSRF token to form
const token = CSRFTokenManager.getToken();
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'csrf_token';
input.value = token;
this.form.appendChild(input);
}
setupEventListeners() {
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
}
async handleSubmit(e) {
e.preventDefault();
const formData = new FormData(this.form);
const data = Object.fromEntries(formData);
try {
const response = await fetch(this.form.action, {
method: this.form.method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': data.csrf_token
},
body: JSON.stringify(data),
credentials: 'include' // Include cookies
});
if (response.ok) {
console.log('Form submitted successfully');
} else {
console.error('Form submission failed');
}
} catch (error) {
console.error('Error submitting form:', error);
}
}
}
// Usage
new SecureFormHandler('myForm');
AJAX Request Protection
// โ
Good: Protect AJAX requests with CSRF tokens
class SecureAjax {
static async request(url, options = {}) {
const token = CSRFTokenManager.getToken();
const headers = {
'Content-Type': 'application/json',
'X-CSRF-Token': token,
...options.headers
};
const response = await fetch(url, {
...options,
headers,
credentials: 'include' // Include cookies
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
static get(url) {
return this.request(url, { method: 'GET' });
}
static post(url, data) {
return this.request(url, {
method: 'POST',
body: JSON.stringify(data)
});
}
static put(url, data) {
return this.request(url, {
method: 'PUT',
body: JSON.stringify(data)
});
}
static delete(url) {
return this.request(url, { method: 'DELETE' });
}
}
// Usage
SecureAjax.post('/api/users', { name: 'John', email: '[email protected]' })
.then(data => console.log('User created:', data))
.catch(error => console.error('Error:', error));
Cookie-based Protection
SameSite Cookie Attribute
// โ
Good: Set SameSite cookie attribute (server-side)
// Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly
// SameSite values:
// - Strict: Cookie only sent in same-site requests
// - Lax: Cookie sent in same-site requests and top-level navigations
// - None: Cookie sent in all requests (requires Secure flag)
// Client-side: Verify SameSite is set
function verifySameSiteCookie() {
const cookies = document.cookie.split(';');
console.log('Cookies:', cookies);
// Check server logs for SameSite attribute
}
Double-Submit Cookie Pattern
// โ
Good: Double-submit cookie pattern
class DoubleSubmitCookie {
static generateToken() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
static setTokenCookie(token) {
// Set as HTTP-only cookie (server-side)
// Set-Cookie: csrf_token=${token}; HttpOnly; Secure; SameSite=Strict
// Also store in meta tag for client-side access
const meta = document.createElement('meta');
meta.name = 'csrf-token';
meta.content = token;
document.head.appendChild(meta);
}
static getTokenFromCookie() {
// Get from meta tag (since HttpOnly cookies aren't accessible)
return document.querySelector('meta[name="csrf-token"]')?.content;
}
static validateToken(token, cookieToken) {
// Server validates: token from request body/header matches cookie
return token === cookieToken;
}
}
// Usage
const token = DoubleSubmitCookie.generateToken();
DoubleSubmitCookie.setTokenCookie(token);
Practical CSRF Protection Examples
Secure API Client
// โ
Good: Secure API client with CSRF protection
class SecureAPIClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.token = CSRFTokenManager.getToken();
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
'X-CSRF-Token': this.token,
...options.headers
};
const config = {
...options,
headers,
credentials: 'include'
};
const response = await fetch(url, config);
if (response.status === 403) {
// CSRF token invalid or expired
console.error('CSRF token validation failed');
throw new Error('CSRF validation failed');
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
}
// Usage
const api = new SecureAPIClient('https://api.example.com');
api.post('/users', { name: 'John', email: '[email protected]' })
.then(user => console.log('User created:', user))
.catch(error => console.error('Error:', error));
Secure Form Submission
// โ
Good: Secure form submission with CSRF protection
class SecureFormSubmitter {
constructor(formSelector) {
this.form = document.querySelector(formSelector);
this.setupForm();
}
setupForm() {
// Add CSRF token
const token = CSRFTokenManager.getToken();
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'csrf_token';
input.value = token;
this.form.appendChild(input);
// Add event listener
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
}
async handleSubmit(e) {
e.preventDefault();
const formData = new FormData(this.form);
const data = Object.fromEntries(formData);
try {
const response = await fetch(this.form.action, {
method: this.form.method || 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': data.csrf_token
},
body: JSON.stringify(data),
credentials: 'include'
});
if (response.ok) {
const result = await response.json();
this.handleSuccess(result);
} else if (response.status === 403) {
this.handleCSRFError();
} else {
this.handleError(response.statusText);
}
} catch (error) {
this.handleError(error.message);
}
}
handleSuccess(result) {
console.log('Form submitted successfully:', result);
// Show success message
alert('Form submitted successfully');
}
handleCSRFError() {
console.error('CSRF token validation failed');
// Refresh page to get new token
window.location.reload();
}
handleError(message) {
console.error('Form submission error:', message);
alert(`Error: ${message}`);
}
}
// Usage
new SecureFormSubmitter('#myForm');
Multi-step Form Protection
// โ
Good: Protect multi-step forms
class MultiStepFormProtection {
constructor(formId) {
this.form = document.getElementById(formId);
this.currentStep = 1;
this.tokens = {};
this.setupForm();
}
setupForm() {
// Generate token for each step
this.generateTokenForStep(this.currentStep);
// Add event listeners to step buttons
const nextButtons = this.form.querySelectorAll('[data-next-step]');
nextButtons.forEach(button => {
button.addEventListener('click', (e) => this.handleNextStep(e));
});
}
generateTokenForStep(step) {
const token = CSRFTokenManager.generateToken();
this.tokens[step] = token;
// Update hidden input
const input = this.form.querySelector('input[name="csrf_token"]');
if (input) {
input.value = token;
} else {
const newInput = document.createElement('input');
newInput.type = 'hidden';
newInput.name = 'csrf_token';
newInput.value = token;
this.form.appendChild(newInput);
}
}
async handleNextStep(e) {
e.preventDefault();
const currentToken = this.tokens[this.currentStep];
const formData = new FormData(this.form);
try {
const response = await fetch('/api/form/validate-step', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': currentToken
},
body: JSON.stringify({
step: this.currentStep,
data: Object.fromEntries(formData)
}),
credentials: 'include'
});
if (response.ok) {
this.currentStep++;
this.generateTokenForStep(this.currentStep);
this.showStep(this.currentStep);
} else {
console.error('Validation failed');
}
} catch (error) {
console.error('Error:', error);
}
}
showStep(step) {
// Hide all steps
this.form.querySelectorAll('[data-step]').forEach(el => {
el.style.display = 'none';
});
// Show current step
const currentStepEl = this.form.querySelector(`[data-step="${step}"]`);
if (currentStepEl) {
currentStepEl.style.display = 'block';
}
}
}
// Usage
new MultiStepFormProtection('multiStepForm');
Best Practices
-
Always use CSRF tokens for state-changing requests:
// โ Good SecureAjax.post('/api/users', data); // โ Bad fetch('/api/users', { method: 'POST', body: JSON.stringify(data) }); -
Use SameSite cookies:
// โ Good (server-side) // Set-Cookie: sessionId=abc; SameSite=Strict; Secure; HttpOnly // โ Bad // Set-Cookie: sessionId=abc -
Validate tokens server-side:
// โ Good - server validates token // โ Bad - only client-side validation
Common Mistakes
-
Not validating CSRF tokens:
// โ Bad - no token validation app.post('/api/users', (req, res) => { // Process request without checking token }); // โ Good - validate token app.post('/api/users', validateCSRFToken, (req, res) => { // Process request }); -
Using GET for state-changing operations:
// โ Bad - GET can be CSRF'd // <img src="/api/users/delete/123"> // โ Good - use POST/PUT/DELETE // Requires CSRF token -
Not including credentials:
// โ Bad - cookies not sent fetch('/api/users', { method: 'POST' }); // โ Good - include credentials fetch('/api/users', { method: 'POST', credentials: 'include' });
Summary
CSRF protection is essential for security. Key takeaways:
- Understand CSRF attacks
- Use CSRF tokens
- Implement SameSite cookies
- Validate server-side
- Protect forms and APIs
- Use secure patterns
- Include credentials
- Validate tokens
Related Resources
- OWASP CSRF Prevention
- SameSite Cookie - MDN
- CSRF Token - OWASP
- Double-Submit Cookie
- Synchronizer Token Pattern
Next Steps
- Learn about Secure Coding Practices
- Explore Dependency Security
- Study Authentication and Authorization
- Practice CSRF protection
- Implement token validation
Comments