TechLead
Lesson 3 of 9
5 min read
Web Security

CSRF Protection

Protect your applications from Cross-Site Request Forgery attacks with tokens, SameSite cookies, and proper validation.

Understanding CSRF Attacks

Cross-Site Request Forgery (CSRF) tricks users into performing unwanted actions on a site where they're authenticated. The attack exploits the trust a site has in the user's browser.

How CSRF Works

<!-- Attacker's malicious page -->
<html>
<body>
  <h1>You won a prize!</h1>

  <!-- Hidden form that auto-submits -->
  <form action="https://bank.com/transfer" method="POST" id="evil-form">
    <input type="hidden" name="to" value="attacker-account">
    <input type="hidden" name="amount" value="10000">
  </form>

  <script>
    document.getElementById('evil-form').submit();
  </script>
</body>
</html>

<!-- If user is logged into bank.com, this transfer will execute! -->

CSRF Protection Methods

1. CSRF Tokens (Synchronizer Token Pattern)

// Server-side: Generate token
const crypto = require('crypto');

function generateCSRFToken(session) {
  const token = crypto.randomBytes(32).toString('hex');
  session.csrfToken = token;
  return token;
}

// Include token in forms
app.get('/transfer', (req, res) => {
  const token = generateCSRFToken(req.session);
  res.render('transfer', { csrfToken: token });
});

// Validate token on submission
app.post('/transfer', (req, res) => {
  const { csrfToken } = req.body;

  if (!csrfToken || csrfToken !== req.session.csrfToken) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }

  // Process the transfer
});
<!-- Include token in form -->
<form action="/transfer" method="POST">
  <input type="hidden" name="csrfToken" value="{{csrfToken}}">
  <input type="text" name="to" placeholder="Recipient">
  <input type="number" name="amount" placeholder="Amount">
  <button type="submit">Transfer</button>
</form>

2. SameSite Cookies

// Set SameSite attribute on cookies
res.cookie('sessionId', sessionId, {
  httpOnly: true,      // Not accessible via JavaScript
  secure: true,        // Only sent over HTTPS
  sameSite: 'strict',  // Not sent on cross-site requests
  maxAge: 3600000,     // 1 hour
});

// SameSite options:
// 'strict' - Cookie never sent on cross-site requests
// 'lax'    - Cookie sent on top-level navigations (default in modern browsers)
// 'none'   - Cookie always sent (requires Secure flag)

3. Double Submit Cookie Pattern

// Set CSRF token in cookie and require it in header
app.use((req, res, next) => {
  if (!req.cookies.csrfToken) {
    const token = crypto.randomBytes(32).toString('hex');
    res.cookie('csrfToken', token, { sameSite: 'strict' });
  }
  next();
});

// Client sends token in header
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': getCookie('csrfToken'),
  },
  body: JSON.stringify(data),
});

// Server validates
app.post('/api/transfer', (req, res) => {
  const cookieToken = req.cookies.csrfToken;
  const headerToken = req.headers['x-csrf-token'];

  if (!cookieToken || cookieToken !== headerToken) {
    return res.status(403).json({ error: 'CSRF validation failed' });
  }

  // Process request
});

4. Using csurf Middleware (Express)

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

app.use(cookieParser());
app.use(csrf({ cookie: true }));

// Token is available as req.csrfToken()
app.get('/form', (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() });
});

// Validation happens automatically on POST/PUT/DELETE

5. Next.js CSRF Protection

// lib/csrf.ts
import Tokens from 'csrf';

const tokens = new Tokens();
const secret = process.env.CSRF_SECRET!;

export function generateToken(): string {
  return tokens.create(secret);
}

export function verifyToken(token: string): boolean {
  return tokens.verify(secret, token);
}

// API route
import { verifyToken } from '@/lib/csrf';

export async function POST(request: Request) {
  const token = request.headers.get('x-csrf-token');

  if (!token || !verifyToken(token)) {
    return Response.json({ error: 'Invalid CSRF token' }, { status: 403 });
  }

  // Process request
}

// React component
function Form({ csrfToken }) {
  const handleSubmit = async (data) => {
    await fetch('/api/submit', {
      method: 'POST',
      headers: {
        'X-CSRF-Token': csrfToken,
      },
      body: JSON.stringify(data),
    });
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

Additional CSRF Mitigations

// Check Origin/Referer headers
function validateOrigin(req) {
  const origin = req.headers.origin || req.headers.referer;
  const allowedOrigins = ['https://mysite.com'];

  if (!origin) return false;

  try {
    const url = new URL(origin);
    return allowedOrigins.includes(url.origin);
  } catch {
    return false;
  }
}

// Require re-authentication for sensitive actions
app.post('/change-password', requireReauth, (req, res) => {
  // Only proceed if user recently re-authenticated
});

// Use custom headers (can't be set cross-origin without CORS)
fetch('/api/data', {
  headers: {
    'X-Requested-With': 'XMLHttpRequest',
  },
});

CSRF Checklist

  • Use CSRF tokens for all state-changing operations
  • Set SameSite=Strict or Lax on session cookies
  • Validate Origin/Referer headers as additional check
  • Use POST for state-changing operations (never GET)
  • Require re-authentication for sensitive actions
  • Consider using custom request headers

Continue Learning