CodeRaptor
Back to Security Vulnerabilities
Medium-High Severity - OWASP Top 10

Cross-Site Request Forgery (CSRF)

Cross-Site Request Forgery (CSRF) is an attack that forces authenticated users to submit requests they did not intend to make. Ranked in OWASP Top 10, CSRF exploits the trust that a web application has in the user's browser by leveraging automatically included credentials (cookies, HTTP authentication). Attackers craft malicious requests that appear legitimate because they carry the victim's authentication credentials. Successful CSRF attacks can transfer funds, change account settings, modify data, or perform any state-changing operation the victim is authorized to perform. Unlike XSS which targets the user, CSRF targets the web application itself.

What Is CSRF?

CSRF exploits the trust that a web application has in the user's browser. When a user is authenticated to a site, their browser automatically includes authentication cookies with every request. Attackers abuse this by creating malicious requests that the browser sends on the user's behalf.

How CSRF Works:

  1. 1.User logs into legitimate site (bank.com) and receives session cookie
  2. 2.User visits malicious site (evil.com) while still logged in
  3. 3.Malicious site triggers hidden request to bank.com
  4. 4.Browser automatically includes authentication cookie
  5. 5.Bank executes request as if user intended it (transfer money, etc.)

CSRF Attack Examples

Hidden Form Attack

Vulnerable Backend (No CSRF Protection)
// Backend endpoint - NO CSRF protection
app.post('/transfer-money', (req, res) => {
  const { toAccount, amount } = req.body;
  const userId = req.session.userId;  // From cookie

  // PROBLEM: No verification this is intentional user action
  transferMoney(userId, toAccount, amount);
  res.json({ success: true });
});
Malicious Page on evil.com
<!-- Hidden form on attacker's site -->
<html>
  <body>
    <h1>Check out this funny cat video!</h1>

    <!-- Invisible form -->
    <form id="attack" action="https://bank.com/transfer-money" method="POST">
      <input type="hidden" name="toAccount" value="attacker123" />
      <input type="hidden" name="amount" value="1000" />
    </form>

    <script>
      // Auto-submit when page loads
      document.getElementById('attack').submit();
    </script>

    <!-- User sees cat video, never knows money was transferred -->
  </body>
</html>

Image Tag Attack (GET Request)

Vulnerable - State Change via GET
// Backend endpoint using GET for state change (BAD!)
app.get('/delete-account', (req, res) => {
  const userId = req.session.userId;

  // PROBLEM: GET should be safe, but this modifies state
  deleteUserAccount(userId);
  res.json({ success: true });
});
Attack via Image Tag
<!-- Attacker embeds this in email or forum post -->
<img src="https://yoursite.com/delete-account" width="1" height="1" />

<!-- Browser automatically makes GET request with cookies
     User's account gets deleted without them knowing -->

CSRF Prevention Methods

1. CSRF Tokens (Synchronizer Token)

Most Common

Generate unique token for each session/request and validate it on the server.

// Backend - Express with csurf middleware
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.get('/form', csrfProtection, (req, res) => {
  // Send CSRF token to client
  res.render('form', { csrfToken: req.csrfToken() });
});

app.post('/transfer-money', csrfProtection, (req, res) => {
  // Token automatically validated by middleware
  const { toAccount, amount } = req.body;
  transferMoney(req.session.userId, toAccount, amount);
  res.json({ success: true });
});

// Frontend - Include token in form
<form action="/transfer-money" method="POST">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
  <input name="toAccount" />
  <input name="amount" />
  <button type="submit">Transfer</button>
</form>

// Or in AJAX header
fetch('/transfer-money', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify(data)
});

2. SameSite Cookie Attribute

Modern Browsers

Prevent browser from sending cookies in cross-site requests.

// Backend - Set SameSite cookie attribute
res.cookie('sessionId', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict'  // or 'lax'
});

// SameSite=Strict: Never send cookie in cross-site requests
// SameSite=Lax: Send cookie on top-level navigation (GET only)
// SameSite=None: Send always (requires Secure flag)

Note: SameSite=Strict may break legitimate cross-site flows (OAuth, payment gateways). Use SameSite=Lax as a good default.

3. Double Submit Cookie Pattern

Stateless

Send CSRF token in both a cookie and request parameter. Server validates they match. Useful for stateless APIs.

// Backend - Generate and send token
const csrfToken = crypto.randomBytes(32).toString('hex');

// Set as cookie
res.cookie('csrf-token', csrfToken, {
  httpOnly: false, // Client needs to read it
  secure: true,
  sameSite: 'strict'
});

// Also send in response body
res.json({ csrfToken });

// Client includes token in request header
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify(data)
});

// Server validates cookie matches header
app.post('/api/transfer', (req, res) => {
  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' });
  }

  // Process request
});

4. Custom Request Headers

AJAX Only

Require custom header that can only be set by JavaScript on same origin. Simple forms cannot send custom headers.

// Client - Send custom header
fetch('/api/data', {
  method: 'POST',
  headers: {
    'X-Requested-With': 'XMLHttpRequest'
  },
  body: JSON.stringify(data)
});

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

  // Process request
});

// Note: Only works for AJAX requests, not regular forms

Framework CSRF Protection

Express.js with csurf Middleware

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

const app = express();
const csrfProtection = csrf({ cookie: true });

app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));

// Send CSRF token to client
app.get('/form', csrfProtection, (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() });
});

// Validate CSRF token automatically
app.post('/process', csrfProtection, (req, res) => {
  // Token validation happens in middleware
  res.send('Data processed successfully');
});

// For AJAX requests
app.get('/api/csrf-token', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

Next.js API Routes

// pages/api/csrf-token.ts
import { NextApiRequest, NextApiResponse } from 'next';
import csrf from 'csrf';

const tokens = new csrf();

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const secret = process.env.CSRF_SECRET || 'default-secret';
  const token = tokens.create(secret);

  res.setHeader(
    'Set-Cookie',
    `csrf-token=${token}; HttpOnly=false; Secure; SameSite=Strict; Path=/`
  );

  res.json({ csrfToken: token });
}

// pages/api/protected-action.ts
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const secret = process.env.CSRF_SECRET || 'default-secret';
  const cookieToken = req.cookies['csrf-token'];
  const headerToken = req.headers['x-csrf-token'] as string;

  if (!tokens.verify(secret, headerToken || cookieToken)) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }

  // Process protected action
  res.json({ success: true });
}

Django (Python)

# settings.py - CSRF middleware enabled by default
MIDDLEWARE = [
    'django.middleware.csrf.CsrfViewMiddleware',
    # other middleware
]

# views.py
from django.views.decorators.csrf import csrf_protect

@csrf_protect
def transfer_money(request):
    if request.method == 'POST':
        # CSRF token validated automatically
        amount = request.POST.get('amount')
        # Process transfer
        return JsonResponse({'success': True})

# Template - Include CSRF token in form
{% csrf_token %}
<form method="POST" action="/transfer">
    {% csrf_token %}
    <input name="amount" />
    <button type="submit">Transfer</button>
</form>

# For AJAX requests
<script>
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'X-CSRFToken': getCookie('csrftoken'),
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ amount: 100 })
});
</script>

Ruby on Rails

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # CSRF protection enabled by default
  protect_from_forgery with: :exception

  # For API endpoints (use null_session or reset_session)
  # protect_from_forgery with: :null_session
end

# In views - Include CSRF token automatically
<%= form_with url: "/transfer" do |form| %>
  <%= form.number_field :amount %>
  <%= form.submit "Transfer" %>
<% end %>

# For AJAX - Rails includes token in meta tags
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="<%= form_authenticity_token %>" />

<script>
const token = document.querySelector('[name="csrf-token"]').content;

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': token,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ amount: 100 })
});
</script>

Origin and Referer Header Validation

Validate Request Origin

Check Origin or Referer headers to ensure requests come from your domain. Use as defense-in-depth alongside tokens.

// Express middleware to validate origin
function validateOrigin(req, res, next) {
  const allowedOrigins = [
    'https://yourdomain.com',
    'https://www.yourdomain.com'
  ];

  // Check Origin header (preferred)
  const origin = req.headers.origin;
  if (origin && !allowedOrigins.includes(origin)) {
    return res.status(403).json({ error: 'Invalid origin' });
  }

  // Fallback to Referer header
  const referer = req.headers.referer || req.headers.referrer;
  if (referer) {
    const refererUrl = new URL(referer);
    const refererOrigin = `${refererUrl.protocol}//${refererUrl.host}`;

    if (!allowedOrigins.includes(refererOrigin)) {
      return res.status(403).json({ error: 'Invalid referer' });
    }
  }

  next();
}

app.post('/api/protected', validateOrigin, csrfProtection, (req, res) => {
  // Process request
});

// Note: Not foolproof (headers can be stripped), use with other protections

CSRF Prevention Best Practices

Use POST for State Changes

Never use GET for operations that modify data

Implement CSRF Tokens

Use framework-provided CSRF protection (Django, Rails, Express)

Set SameSite Cookies

Use SameSite=Lax or Strict on session cookies

Validate Origin Header

Check Origin/Referer matches your domain

Use HTTPOnly Cookies

Prevent JavaScript access to cookies

Require Re-auth

Ask for password on sensitive operations

How CodeRaptor Detects CSRF Vulnerabilities

CodeRaptor automatically scans for missing CSRF protection on state-changing endpoints.

Detects POST/PUT/DELETE without CSRF tokens
Identifies GET requests that modify state
Checks for missing SameSite cookie attributes
Validates framework CSRF middleware usage
Flags missing Origin/Referer checks
Suggests proper CSRF implementation
Try CodeRaptor Free