Skip to content

CSRF Protection — Complete Implementation Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about CSRF Protection. We cover key concepts, practical examples, and best practices to help you master this topic.

Cross-Site Request Forgery (CSRF) is an attack that tricks an authenticated user into executing unwanted actions on a web application where they are currently authenticated, and CSRF protection prevents this by ensuring that requests originate from the legitimate application.

What You'll Learn

By the end of this lesson, you will implement CSRF protection using synchronized tokens, double-submit cookies, and SameSite cookies, understand when CSRF protection is needed, and secure both traditional web apps and APIs.

Why It Matters

CSRF Attacks exploit the browser's automatic inclusion of cookies with every request. Without CSRF protection, an attacker can forge requests on behalf of authenticated users. Doda Browser's web interface uses CSRF tokens on all state-changing requests to protect user accounts.

Real-World Use

A user is logged into their banking website. In another tab, they visit a malicious site that contains a hidden form submitting a money transfer request to the bank's API. Since the user's session cookie is automatically included, the transfer goes through. CSRF protection would block this because the malicious form cannot include the CSRF token.

CSRF Attack Flow

sequenceDiagram
    participant User
    participant Bank as Banking App
    participant Attacker as Malicious Site

    User->>Bank: Login
    Bank-->>User: Session cookie set
    User->>Attacker: Browse malicious site
    Attacker->>User: Hidden form targeting Bank
    User->>User: Auto-submit form
    User->>Bank: POST /transfer (with session cookie)
    Note over Bank: No CSRF check - request processed!
    Bank-->>User: Transfer completed (unauthorized)

Synchronized Token Pattern

const express = require("express");
const crypto = require("crypto");
const cookieParser = require("cookie-parser");

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

const TOKEN_SECRET = crypto.randomBytes(32).toString("hex");

function generateCsrfToken() {
  const token = crypto.randomBytes(32).toString("hex");
  const hmac = crypto.createHmac("sha256", TOKEN_SECRET)
    .update(token).digest("hex");
  return `${token}.${hmac}`;
}

function verifyCsrfToken(token) {
  const [randomPart, signature] = token.split(".");
  const expected = crypto.createHmac("sha256", TOKEN_SECRET)
    .update(randomPart).digest("hex");
  return signature === expected;
}

app.get("/api/csrf-token", (req, res) => {
  const token = generateCsrfToken();
  res.cookie("csrf-token", token, {
    httpOnly: false,  // JavaScript needs to read this
    secure: true,
    sameSite: "strict",
  });
  res.json({ csrfToken: token });
});

function csrfProtection(req, res, next) {
  if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
    return next();
  }

  const headerToken = req.headers["x-csrf-token"];
  const cookieToken = req.cookies["csrf-token"];

  if (!headerToken || !cookieToken) {
    console.log(`[CSRF] Missing token`);
    return res.status(403).json({ error: "CSRF token required" });
  }

  if (headerToken !== cookieToken) {
    console.log(`[CSRF] Token mismatch`);
    return res.status(403).json({ error: "CSRF token mismatch" });
  }

  if (!verifyCsrfToken(headerToken)) {
    console.log(`[CSRF] Invalid token signature`);
    return res.status(403).json({ error: "Invalid CSRF token" });
  }

  next();
}

app.post("/api/transfer", csrfProtection, (req, res) => {
  const { amount, to } = req.body;
  console.log(`[Transfer] Processing: ${amount} to ${to}`);
  res.json({ message: "Transfer processed" });
});

app.listen(3000);

CSRF Protection with SameSite Cookies

// Modern CSRF protection using SameSite cookies
// SameSite=Strict prevents the cookie from being sent on cross-origin requests

app.use(session({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: "strict",  // Built-in CSRF protection
    maxAge: 24 * 60 * 60 * 1000,
  },
}));

// For cases where SameSite is not sufficient:
// - POST forms from same-site but different origins
// - Legacy browsers that don't support SameSite

function doubleSubmitCookie(req, res, next) {
  if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
    return next();
  }

  const cookieValue = req.cookies["csrf-double"];
  const headerValue = req.headers["x-csrf-token"];

  if (!cookieValue || !headerValue) {
    return res.status(403).json({ error: "CSRF tokens missing" });
  }

  if (cookieValue !== headerValue) {
    return res.status(403).json({ error: "CSRF tokens don't match" });
  }

  next();
}

Django CSRF Protection

# Django has built-in CSRF protection
# settings.py
MIDDLEWARE = [
    "django.middleware.csrf.CsrfViewMiddleware",
    # ...
]

# In templates, include the CSRF token:
# <form method="POST">
#     {% csrf_token %}
#     ...
# </form>

# For Django REST Framework:
from rest_framework.views import APIView
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie

@method_decorator(ensure_csrf_cookie, name="dispatch")
class CSRFView(APIView):
    def get(self, request):
        return Response({"csrfToken": self.get_csrf_token(request)})

    def get_csrf_token(self, request):
        from django.middleware.csrf import get_token
        return get_token(request)

# For AJAX requests, include the CSRF token:
# const csrfToken = document.cookie
#     .split('; ')
#     .find(row => row.startsWith('csrftoken'))
#     .split('=')[1];

# fetch('/api/data', {
#     method: 'POST',
#     headers: {
#         'X-CSRFToken': csrfToken,
#     },
#     credentials: 'include',
# });

Common Mistakes

  1. Disabling CSRF protection for API endpoints that use cookie authentication.
  2. Using GET requests for state-changing operations (CSRF via image tags).
  3. Not protecting CSRF token endpoints with authentication (anyone can get a token).
  4. Using the same CSRF token for the entire session instead of per-request tokens.
  5. Not including CSRF protection on Websocket connections.
  6. Relying solely on CORS for CSRF protection (CORS blocks reading responses, not sending requests).

Practice Questions

  1. How does SameSite=Strict prevent CSRF attacks?

SameSite=Strict tells the browser to never send the cookie with cross-origin requests. Since CSRF attacks rely on the browser automatically including cookies, SameSite prevents the cookie from being sent to the attacker's forged request.

  1. What is the difference between synchronized token and double-submit cookie?

Synchronized token: Server generates a token, stores it in session, and requires it in requests. Double-submit: Server sets a cookie with a random value, and the client must send the same value in a header. No server-side storage needed.

  1. Why do APIs not need CSRF protection when using token-based auth?

CSRF attacks exploit automatic cookie inclusion. Token-based auth (Bearer tokens in Authorization header) is not automatically included by the browser. The attacker's malicious site cannot read the token to include it.

  1. Challenge: Implement CSRF protection for a traditional session-based web app with per-request tokens, token rotation after each state-changing request, defense against token leakage via Referer headers, and support for mobile API clients that cannot read cookies.

FAQ

Is CSRF protection still needed with SameSite cookies?

SameSite provides broad protection but is not universal. Older browsers don't support it. Some legitimate cross-site flows need SameSite=None. Defense-in-depth recommends CSRF tokens in addition to SameSite.

Do I need CSRF protection for APIs?

Only if using cookie-based authentication. APIs using Bearer tokens (JWT, API keys) are not vulnerable to CSRF because the browser does not automatically include Authorization headers.

How do mobile apps handle CSRF?

Mobile apps are not vulnerable to CSRF because they don't use cookies in the same way as browsers. They typically use token-based auth with stored access tokens.

What is the difference between CORS and CSRF?

CORS controls which origins can read responses from your API. CSRF is about unauthorized state-changing requests using the user's cookies. CORS does not prevent CSRF — the malicious request reaches your server regardless of CORS settings.

Mini Project: CSRF Testing Tool

Build a CLI tool that tests web applications for CSRF vulnerabilities by submitting forged requests without CSRF tokens.

import requests
import sys

class CSRFScanner:
    def __init__(self, base_url):
        self.base_url = base_url
        self.session = requests.Session()

    def check_csrf_on_form(self, form_url, form_data, action_url=None):
        """Test if a form submission is protected by CSRF."""
        print(f"Testing CSRF protection on {form_url}")

        initial = self.session.get(form_url)
        csrf_token = self._extract_csrf_token(initial.text)

        if csrf_token:
            print(f"  CSRF token found: {csrf_token[:20]}...")
        else:
            print(f"  WARNING: No CSRF token found in form!")

        target = action_url or form_url
        result = self.session.post(target, data=form_data)

        if result.status_code in (200, 201, 302):
            print(f"  Request succeeded (status {result.status_code})")
            if not csrf_token:
                print(f"  VULNERABLE: No CSRF protection detected!")
                return False
        elif result.status_code in (403, 401):
            print(f"  Request blocked (status {result.status_code})")
            print(f"  CSRF protection present")
            return True
        else:
            print(f"  Unknown status: {result.status_code}")

        return True

    def _extract_csrf_token(self, html):
        import re
        patterns = [
            r'name="csrf_token" value="([^"]+)"',
            r'name="csrfmiddlewaretoken" value="([^"]+)"',
            r'csrf-token[^>]*content="([^"]+)"',
            r'X-CSRF-Token[^>]*content="([^"]+)"',
        ]
        for pattern in patterns:
            match = re.search(pattern, html)
            if match:
                return match.group(1)
        return None

scanner = CSRFScanner("http://localhost:3000")
scanner.check_csrf_on_form("/login", {"email": "test@test.com", "password": "test"})

What's Next

Learn about auth middleware for backend request authentication, then explore security headers for web application security.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro