Skip to content
All essays
WebMarch 27, 202417 min

CSRF Prevention: Complete Guide to Cross-Site Request Forgery Protection

Master CSRF attack prevention with tokens, same-site cookies, and best practices for securing web applications

Ü
Ümit Uz
Mobile & Full Stack Developer

Cross-Site Request Forgery (CSRF) attacks force authenticated users to execute unwanted actions on web applications where they're authenticated. This guide covers comprehensive CSRF protection strategies.

Understanding CSRF

How CSRF Works

  1. 1User logs into example.com
  2. 2Server sets session cookie
  3. 3User visits malicious site evil.com
  4. 4Evil site sends request to example.com
  5. 5Browser automatically includes session cookie
  6. 6Server processes request as legitimate user

``html <!-- Malicious site evil.com --> <form action="https://example.com/transfer" method="POST"> <input type="hidden" name="to" value="attacker"> <input type="hidden" name="amount" value="10000"> <input type="submit" value="Click me!"> </form>

<!-- Or auto-submit with JavaScript --> <body onload="document.forms[0].submit()"> <form action="https://example.com/transfer" method="POST"> <input type="hidden" name="to" value="attacker"> <input type="hidden" name="amount" value="10000"> </form> </body>


## CSRF Token Protection

### Generating CSRF Tokens

const crypto = require('crypto');

class CSRFProtection { constructor({ secret, sessionStore }) { this.secret = secret; this.sessionStore = sessionStore; }

// Generate secure random token generateToken() { return crypto.randomBytes(32).toString('hex'); }

// Create token with HMAC signature async createToken(sessionId) { const token = this.generateToken(); const timestamp = Date.now();

// Create signed token const signedToken = this.signToken(token, timestamp);

// Store in session await this.sessionStore.set(csrf:${sessionId}, { token, timestamp, used: false });

return signedToken; }

// Sign token with HMAC signToken(token, timestamp) { const data = ${token}|${timestamp}; const hmac = crypto .createHmac('sha256', this.secret) .update(data) .digest('hex');

return Buffer.from(${data}|${hmac}).toString('base64url'); }

// Verify token async verifyToken(sessionId, signedToken) { try { // Decode and parse token const parts = Buffer.from(signedToken, 'base64url').toString().split('|'); const [token, timestamp, signature] = parts;

// Verify signature const data = ${token}|${timestamp}; const expectedHmac = crypto .createHmac('sha256', this.secret) .update(data) .digest('hex');

if (signature !== expectedHmac) { throw new Error('Invalid token signature'); }

// Check token hasn't been used const stored = await this.sessionStore.get(csrf:${sessionId}); if (!stored || stored.token !== token || stored.used) { throw new Error('Invalid or used token'); }

// Check token age (max 1 hour) const age = Date.now() - parseInt(timestamp); if (age > 3600000) { throw new Error('Token expired'); }

// Mark token as used (one-time use) await this.sessionStore.set(csrf:${sessionId}, { ...stored, used: true });

return true; } catch (error) { console.error('CSRF verification failed:', error); return false; } } }


### Express.js Integration

const express = require('express'); const cookieParser = require('cookie-parser'); const csrf = require('csurf'); const app = express();

// Using csurf middleware const csrfProtection = csrf({ cookie: true });

app.use(express.json()); app.use(cookieParser());

// Provide CSRF token to frontend app.get('/api/csrf-token', csrfProtection, (req, res) => { res.json({ csrfToken: req.csrfToken() }); });

// Protected routes app.post('/api/transfer', csrfProtection, async (req, res) => { const { to, amount } = req.body;

// Process transfer await processTransfer(req.user.id, to, amount);

res.json({ success: true }); });

// Custom CSRF middleware class CustomCSRF { constructor(csrfProtection) { this.protection = csrfProtection; }

middleware() { return (req, res, next) => { // Skip CSRF for GET requests (safe methods) if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { return next(); }

this.protection(req, res, next); }; }

// Middleware to provide token provideToken() { return (req, res, next) => { res.locals.csrfToken = req.csrfToken(); next(); }; } }

const customCSRF = new CustomCSRF(csrfProtection()); app.use(customCSRF.middleware()); app.use(customCSRF.provideToken());


## Same-Site Cookies

### Cookie Configuration

// Express session with SameSite cookies const session = require('express-session');

app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: process.env.NODE_ENV === 'production', // HTTPS only in production sameSite: 'strict', // 'strict' | 'lax' | 'none' maxAge: 24 * 60 * 60 * 1000 // 24 hours } }));

// Setting cookies manually res.cookie('session', sessionId, { httpOnly: true, secure: true, sameSite: 'strict', domain: '.example.com', path: '/' });


### Same-Site Attribute Values

``javascript
// Strict: Never send cookies with cross-site requests
res.cookie('session', value, { sameSite: 'strict' });

// Lax: Send cookies with safe cross-site requests (GET)
res.cookie('session', value, { sameSite: 'lax' });

// None: Send cookies with all cross-site requests (requires Secure)
res.cookie('session', value, { sameSite: 'none', secure: true });

Frontend Implementation

React with CSRF Tokens

javascript
import { useEffect, useState } from 'react';

function CSRFProtectedForm() {
  const [csrfToken, setCsrfToken] = useState('');

  useEffect(() => {
    // Fetch CSRF token on mount
    fetch('/api/csrf-token')
      .then(res => res.json())
      .then(data => setCsrfToken(data.csrfToken));
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();

    const formData = {
      to: 'recipient@example.com',
      amount: 100,
      _csrf: csrfToken // Include CSRF token
    };

    const response = await fetch('/api/transfer', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken // Or via header
      },
      credentials: 'include', // Include cookies
      body: JSON.stringify(formData)
    });

    const result = await response.json();
    console.log('Transfer result:', result);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="hidden" name="_csrf" value={csrfToken} />

      <input
        type="text"
        name="to"
        placeholder="Recipient"
        required
      />

      <input
        type="number"
        name="amount"
        placeholder="Amount"
        required
      />

      <button type="submit">Transfer</button>
    </form>
  );
}

Axios Configuration

javascript
import axios from 'axios';

// Create axios instance with CSRF
const api = axios.create({
  baseURL: '/api',
  withCredentials: true // Include cookies
});

// Request interceptor to add CSRF token
api.interceptors.request.use(async (config) => {
  // Fetch CSRF token if not in cache
  if (!config.headers['X-CSRF-Token']) {
    const { data } = await axios.get('/api/csrf-token');
    config.headers['X-CSRF-Token'] = data.csrfToken;
  }

  return config;
}, error => Promise.reject(error));

// Usage
async function transferFunds(to, amount) {
  try {
    const response = await api.post('/transfer', { to, amount });
    return response.data;
  } catch (error) {
    console.error('Transfer failed:', error);
  }
}
javascript
class DoubleSubmitCSRF {
  // Generate token and set in both cookie and header
  static middleware() {
    return (req, res, next) => {
      const token = crypto.randomBytes(32).toString('hex');

      // Set in cookie
      res.cookie('csrf_token', token, {
        httpOnly: false, // Must be accessible to JavaScript
        secure: true,
        sameSite: 'strict'
      });

      // Provide token to template
      res.locals.csrfToken = token;

      next();
    };
  }

  // Verify token matches
  static verify() {
    return (req, res, next) => {
      const cookieToken = req.cookies.csrf_token;
      const headerToken = req.headers['x-csrf-token'];
      const bodyToken = req.body._csrf;

      // Check if token exists
      if (!cookieToken) {
        return res.status(403).json({ error: 'Missing CSRF cookie' });
      }

      // Verify token matches
      if (headerToken !== cookieToken && bodyToken !== cookieToken) {
        return res.status(403).json({ error: 'Invalid CSRF token' });
      }

      next();
    };
  }
}

app.use(DoubleSubmitCSRF.middleware());
app.post('/api/protected', DoubleSubmitCSRF.verify(), (req, res) => {
  res.json({ success: true });
});

Additional Protection Strategies

Origin/Referer Header Validation

javascript
function validateOrigin(allowedOrigins) {
  return (req, res, next) => {
    const origin = req.headers.origin;
    const referer = req.headers.referer;

    // Skip for same-origin requests
    if (req.headers.host && origin) {
      const originHost = new URL(origin).host;
      const reqHost = req.headers.host;

      if (originHost === reqHost) {
        return next();
      }
    }

    // Validate against allowed origins
    if (origin && allowedOrigins.includes(origin)) {
      return next();
    }

    // Check referer as fallback
    if (referer) {
      const refererHost = new URL(referer).host;
      if (allowedOrigins.some(allowed => refererHost.includes(new URL(allowed).host))) {
        return next();
      }
    }

    res.status(403).json({ error: 'Invalid origin' });
  };
}

app.post('/api/protected',
  validateOrigin(['https://example.com', 'https://app.example.com']),
  (req, res) => {
    res.json({ success: true });
  }
);

Custom Headers

javascript
// Require custom header that browsers can't set from forms
function requireCustomHeader() {
  return (req, res, next) => {
    // Check for custom request header
    if (!req.headers['x-requested-with']) {
      return res.status(403).json({ error: 'Missing required header' });
    }
    next();
  };
}

// Frontend must add header
fetch('/api/protected', {
  headers: {
    'X-Requested-With': 'XMLHttpRequest'
  }
});

Testing CSRF Protection

Automated Tests

javascript
const request = require('supertest');
const app = require('./app');

describe('CSRF Protection', () => {
  it('should reject requests without CSRF token', async () => {
    const response = await request(app)
      .post('/api/transfer')
      .send({ to: 'attacker', amount: 1000 })
      .expect(403);

    expect(response.body.error).toContain('CSRF');
  });

  it('should accept requests with valid CSRF token', async () => {
    // Get CSRF token
    const tokenResponse = await request(app)
      .get('/api/csrf-token')
      .expect(200);

    const { csrfToken } = tokenResponse.body;

    // Use token in request
    const response = await request(app)
      .post('/api/transfer')
      .set('X-CSRF-Token', csrfToken)
      .send({ to: 'recipient', amount: 100 })
      .expect(200);

    expect(response.body.success).toBe(true);
  });
});

Best Practices

  1. 1Use SameSite=Strict: Best protection, but may break some functionality
  2. 2Implement CSRF Tokens: Required for state-changing operations
  3. 3Verify Origin/Referer: Additional layer of protection
  4. 4GET Requests Safe: Never change state with GET requests
  5. 5Token Per Session: Generate unique token per session
  6. 6Token Per Request: Even better: rotate tokens per request
  7. 7Short Expiration: Tokens should expire quickly (1 hour)
  8. 8Secure Cookie: Always use HttpOnly and Secure flags

Conclusion

CSRF protection is essential for web application security. Combine multiple defenses: SameSite cookies, CSRF tokens, and origin validation. Implement defense in depth, test thoroughly, and stay updated on emerging threats and best practices.

Related essays

Next essay
Message Queues with RabbitMQ: A Complete Guide