Skip to main content
โšก Calmops

CSRF Protection in JavaScript

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));
// โœ… 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
}
// โœ… 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

  1. Always use CSRF tokens for state-changing requests:

    // โœ… Good
    SecureAjax.post('/api/users', data);
    
    // โŒ Bad
    fetch('/api/users', { method: 'POST', body: JSON.stringify(data) });
    
  2. Use SameSite cookies:

    // โœ… Good (server-side)
    // Set-Cookie: sessionId=abc; SameSite=Strict; Secure; HttpOnly
    
    // โŒ Bad
    // Set-Cookie: sessionId=abc
    
  3. Validate tokens server-side:

    // โœ… Good - server validates token
    // โŒ Bad - only client-side validation
    

Common Mistakes

  1. 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
    });
    
  2. 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
    
  3. 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

Next Steps

Comments