Back to Learning Center
mediumA01:2021CWE-352

Cross-Site Request Forgery (CSRF)

Cross-Site Request Forgery (CSRF) is a web security vulnerability that tricks authenticated users into performing actions they didn't intend to perform. By exploiting the browser's automatic inclusion of cookies in requests, attackers can force victims to transfer funds, change passwords, or modify account settings—all without their knowledge.

What is CSRF?

When you're logged into a website, your browser automatically sends cookies with every request to that site. CSRF exploits this by tricking your browser into making requests to a site where you're authenticated, using your existing session.

For a CSRF attack to work, three conditions must be met:

  • The victim must be authenticated to the target site
  • The target site relies on cookies for session management
  • There's no unpredictable parameter in the request

How CSRF Works

Basic CSRF Attack

An attacker creates a malicious page that submits a form to the vulnerable site:

html
<!-- Attacker's malicious page: evil-site.com/attack.html -->
<html>
<body>
  <h1>You Won a Prize!</h1>
  
  <!-- Hidden form that auto-submits -->
  <form id="csrf" action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="to" value="attacker-account" />
    <input type="hidden" name="amount" value="10000" />
  </form>
  
  <script>
    document.getElementById('csrf').submit();
  </script>
</body>
</html>

<!-- When victim visits this page while logged into bank.com,
     their browser automatically sends their session cookie,
     completing the transfer to the attacker's account -->

GET-Based CSRF

If state-changing actions use GET requests, attacks become even simpler:

html
<!-- Image tag that triggers action -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" />

<!-- Or via link -->
<a href="https://bank.com/delete-account">Click for free prize!</a>

<!-- These work because GET requests are sent automatically
     when loading images or clicking links -->

JSON-Based CSRF

APIs that accept JSON can still be vulnerable if they don't validate Content-Type:

html
<!-- Form submission with text/plain (no preflight required) -->
<form action="https://api.target.com/user/email" method="POST" enctype="text/plain">
  <input name='{"email":"attacker@evil.com","ignore":"' value='"}' />
</form>

<!-- Resulting body: {"email":"attacker@evil.com","ignore":"=\"} -->
<!-- If server parses this as JSON, attack succeeds -->

Vulnerable Code Example

javascript
// Vulnerable: No CSRF protection
app.post('/change-email', (req, res) => {
  const userId = req.session.userId; // From cookie
  const newEmail = req.body.email;
  
  // Dangerous! No verification that request came from our site
  db.users.update(userId, { email: newEmail });
  res.send('Email updated');
});

// Attacker can change victim's email by getting them
// to visit a page with a form that POSTs to this endpoint

Prevention Strategies

1. CSRF Tokens (Synchronizer Token Pattern)

The gold standard for CSRF protection. Generate a unique, unpredictable token per session and require it in all state-changing requests:

javascript
import crypto from 'crypto';

// Generate CSRF token on login
app.post('/login', (req, res) => {
  // ... authenticate user ...
  req.session.csrfToken = crypto.randomBytes(32).toString('hex');
  res.redirect('/dashboard');
});

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

// Validate token on state-changing requests
function csrfProtection(req, res, next) {
  const token = req.body._csrf || req.headers['x-csrf-token'];
  
  if (!token || token !== req.session.csrfToken) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }
  
  next();
}

app.post('/change-email', csrfProtection, (req, res) => {
  // Safe to process - request is verified
  db.users.update(req.session.userId, { email: req.body.email });
  res.send('Email updated');
});

2. SameSite Cookies

SameSite cookies prevent the browser from sending cookies with cross-site requests:

javascript
app.use(session({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict', // or 'lax'
    maxAge: 3600000
  }
}));

// SameSite values:
// 'strict' - Cookie never sent cross-site (most secure)
// 'lax'    - Cookie sent with top-level navigations (GET only)
// 'none'   - Cookie always sent (requires secure: true)

// Note: Chrome defaults to 'lax' since 2021

SameSite provides defense-in-depth but should not be your only protection. Use it alongside CSRF tokens.

For stateless applications that can't store tokens server-side:

javascript
// Set CSRF cookie on page load
app.get('/app', (req, res) => {
  const csrfToken = crypto.randomBytes(32).toString('hex');
  
  res.cookie('csrf_token', csrfToken, {
    httpOnly: false, // JavaScript needs to read this
    secure: true,
    sameSite: 'strict'
  });
  
  res.render('app');
});

// Client-side: Read cookie and send as header
fetch('/api/action', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': getCookie('csrf_token')
  },
  body: JSON.stringify(data)
});

// Server: Compare cookie value with header
function doubleSubmitCheck(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();
}

4. Custom Request Headers

Cross-origin requests with custom headers trigger CORS preflight, which blocks the request:

javascript
// Client: Always send custom header
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest' // Custom header
  },
  body: JSON.stringify({ amount: 100 })
});

// Server: Require custom header
app.post('/api/*', (req, res, next) => {
  if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
    return res.status(403).json({ error: 'Invalid request' });
  }
  next();
});

Framework Protection

Most modern frameworks include built-in CSRF protection:

javascript
// Express with csurf (deprecated, use alternatives)
import csrf from 'csurf';
app.use(csrf({ cookie: true }));

// Next.js - use iron-session with CSRF
// Django - {% csrf_token %} in templates
// Rails - protect_from_forgery built-in
// Laravel - @csrf in Blade templates

Security Checklist

  • Never use GET for state-changing operations
  • Implement CSRF tokens for all state-changing requests
  • Set SameSite=Strict or Lax on session cookies
  • Verify Content-Type on JSON APIs
  • Use framework-provided CSRF protection when available
  • Require custom headers for AJAX requests
  • Verify Origin/Referer headers as additional defense
  • Re-authenticate for sensitive actions (password changes, transfers)