In the previous tutorial, you learned how HTTPS and TLS protect data in transit. But even with HTTPS, your application can be tricked into performing actions on behalf of a user — without the user knowing. This is called CSRF (Cross-Site Request Forgery).

What Is CSRF?

CSRF is an attack where a malicious website tricks your browser into sending a request to another website where you are already logged in. The browser automatically includes your cookies — so the server thinks the request came from you.

Here is a simple example:

  1. You log in to your bank at bank.com. The browser stores a session cookie.
  2. You visit a malicious website evil.com.
  3. evil.com has a hidden form that submits a POST request to bank.com/transfer?to=attacker&amount=1000.
  4. Your browser sends the request with your bank cookies attached.
  5. The bank processes the transfer because the cookies are valid.

You never clicked “transfer.” The malicious site did it for you.

Why Cookies Are the Root Cause

CSRF works because browsers automatically attach cookies to every request to a domain — regardless of where the request originated. If you are logged in to bank.com, any website can trigger a request to bank.com and your cookies will be included.

This is called ambient authentication. The browser handles authentication automatically, and that is what attackers exploit.

Important: CSRF does not steal your data. The attacker cannot read the response. But they can make your browser perform actions — transfer money, change your email, delete your account.

A Real CSRF Attack

In 2008, a CSRF vulnerability in a popular router (UTStarcom) allowed attackers to change DNS settings by embedding a hidden image tag in a webpage:

<!-- This is the attack — just an image tag -->
<img src="http://192.168.1.1/admin/dns?server=evil-dns.com" />

When a logged-in admin visited any page containing this HTML, the router changed its DNS settings. The attacker did not need the admin’s password.

CSRF Prevention Strategy #1: CSRF Tokens

The most common defense is the synchronizer token pattern. The server generates a random, unpredictable token and includes it in every form. When the form is submitted, the server checks if the token is valid.

The attacker cannot read the token because they cannot access your page’s HTML (same-origin policy prevents that).

CSRF Token Flow

  1. Server generates a random token and stores it in the user’s session
  2. Server includes the token as a hidden field in every HTML form
  3. User submits the form — the token is sent along with the request
  4. Server verifies the token matches the one stored in the session
  5. If the token is missing or wrong, the server rejects the request

CSRF Tokens in Go (Gorilla CSRF)

package main

import (
    "fmt"
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()

    r.HandleFunc("/form", showForm).Methods("GET")
    r.HandleFunc("/submit", handleSubmit).Methods("POST")

    // 32-byte authentication key for CSRF tokens
    csrfMiddleware := csrf.Protect(
        []byte("32-byte-long-auth-key-here123456"),
        csrf.Secure(true), // Requires HTTPS
    )

    fmt.Println("Server running on :8080")
    http.ListenAndServe(":8080", csrfMiddleware(r))
}

func showForm(w http.ResponseWriter, r *http.Request) {
    // Include the CSRF token in the form
    fmt.Fprintf(w, `
        <form method="POST" action="/submit">
            <input type="hidden" name="gorilla.csrf.Token" value="%s">
            <input type="text" name="email">
            <button type="submit">Update Email</button>
        </form>
    `, csrf.Token(r))
}

func handleSubmit(w http.ResponseWriter, r *http.Request) {
    // Token is verified automatically by the middleware
    fmt.Fprintf(w, "Email updated successfully!")
}

CSRF Tokens in Python (Django)

Django includes CSRF protection by default. You do not need to install anything:

# Django template — the {% csrf_token %} tag inserts the token
# templates/form.html
"""
<form method="POST" action="/submit/">
    {% csrf_token %}
    <input type="text" name="email">
    <button type="submit">Update Email</button>
</form>
"""

# views.py — Django verifies the token automatically
from django.shortcuts import render
from django.http import HttpResponse

def show_form(request):
    return render(request, "form.html")

def handle_submit(request):
    if request.method == "POST":
        email = request.POST.get("email")
        # Token was verified by Django middleware automatically
        return HttpResponse(f"Email updated to {email}")

Django’s CsrfViewMiddleware is enabled by default in every new project. If you disable it, you lose CSRF protection.

CSRF Tokens in Express.js

import express from "express";
import csrf from "csurf";
import cookieParser from "cookie-parser";

const app = express();
app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));

// Set up CSRF protection
const csrfProtection = csrf({ cookie: true });

app.get("/form", csrfProtection, (req, res) => {
  // Include the token in the form
  res.send(`
    <form method="POST" action="/submit">
      <input type="hidden" name="_csrf" value="${req.csrfToken()}">
      <input type="text" name="email">
      <button type="submit">Update Email</button>
    </form>
  `);
});

app.post("/submit", csrfProtection, (req, res) => {
  // Token is verified automatically
  res.send("Email updated successfully!");
});

app.listen(3000, () => console.log("Server on :3000"));

Note: The csurf library is deprecated. For new projects, consider using csrf-csrf or the double-submit cookie pattern instead.

CSRF Prevention Strategy #2: SameSite Cookies

Modern browsers support the SameSite cookie attribute, which tells the browser when to include cookies in cross-site requests.

ValueBehaviorCSRF Protection
StrictCookie only sent for same-site requestsStrong — blocks all cross-site cookie sending
LaxCookie sent for top-level GET navigations onlyGood — blocks cross-site POST, allows links
NoneCookie always sent (requires Secure flag)None — must use other CSRF defenses

Setting SameSite in Go

http.SetCookie(w, &http.Cookie{
    Name:     "session_id",
    Value:    sessionToken,
    HttpOnly: true,
    Secure:   true,
    SameSite: http.SameSiteLaxMode, // or http.SameSiteStrictMode
    Path:     "/",
})

Setting SameSite in Python (Django)

# settings.py
SESSION_COOKIE_SAMESITE = "Lax"   # "Strict", "Lax", or "None"
SESSION_COOKIE_SECURE = True       # Requires HTTPS
SESSION_COOKIE_HTTPONLY = True      # Prevents JavaScript access

Setting SameSite in JavaScript (Express)

res.cookie("session_id", sessionToken, {
  httpOnly: true,
  secure: true,
  sameSite: "lax", // "strict", "lax", or "none"
  path: "/",
});

Important: SameSite=Lax is the default in modern browsers (Chrome, Firefox, Edge). But do not rely on this alone — older browsers may not support it. Use CSRF tokens as well.

CSRF Prevention Strategy #3: Custom Request Headers

For API requests made with JavaScript (fetch, Axios), you can use a custom header. Browsers enforce the same-origin policy on custom headers — a cross-origin request cannot set custom headers without a CORS preflight.

// Frontend: add a custom header to every API request
fetch("/api/update-email", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest", // Custom header
  },
  body: JSON.stringify({ email: "alex@example.com" }),
});
// Backend: verify the custom header exists
func csrfHeaderMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method != "GET" && r.Method != "HEAD" {
            if r.Header.Get("X-Requested-With") == "" {
                http.Error(w, "Missing CSRF header", http.StatusForbidden)
                return
            }
        }
        next.ServeHTTP(w, r)
    })
}

This works because a malicious site cannot add custom headers to a cross-origin request without triggering a CORS preflight — which the server can reject.

This pattern does not require server-side session storage. The server sets a random value in a cookie AND expects the same value in the request body or header.

  1. Server sets a cookie: csrf_token=abc123
  2. Frontend reads the cookie and includes the value in a header: X-CSRF-Token: abc123
  3. Server compares the cookie value with the header value
  4. They match — the request is legitimate

An attacker can cause the browser to send the cookie, but they cannot read it (same-origin policy). So they cannot include the value in the header.

# Python (Flask) double-submit cookie example
import secrets
from flask import Flask, request, make_response, jsonify

app = Flask(__name__)

@app.route("/api/csrf-token", methods=["GET"])
def get_csrf_token():
    token = secrets.token_hex(32)
    response = make_response(jsonify({"token": token}))
    response.set_cookie(
        "csrf_token", token,
        httponly=False,  # Frontend must read this cookie
        secure=True,
        samesite="Lax"
    )
    return response

@app.route("/api/update-email", methods=["POST"])
def update_email():
    cookie_token = request.cookies.get("csrf_token")
    header_token = request.headers.get("X-CSRF-Token")

    if not cookie_token or cookie_token != header_token:
        return jsonify({"error": "CSRF validation failed"}), 403

    # Process the request
    return jsonify({"message": "Email updated"})

JWT APIs: CSRF Is Not a Problem

If your API uses JWT tokens in the Authorization header instead of cookies, CSRF is not a concern. The browser does not automatically include the Authorization header — your JavaScript code must add it explicitly.

// JWT in Authorization header — no CSRF risk
fetch("/api/update-email", {
  method: "POST",
  headers: {
    "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ email: "alex@example.com" }),
});

The attacker’s website cannot add the Authorization header to a cross-origin request. So CSRF does not apply.

Warning: If you store your JWT in a cookie (some frameworks do this), CSRF applies again. Use SameSite and CSRF tokens in that case.

Which Strategy Should You Use?

SituationRecommended Strategy
Server-rendered HTML formsCSRF tokens + SameSite cookies
SPA with cookie-based authDouble-submit cookie + SameSite
SPA with JWT in headersNo CSRF protection needed
API with cookie-based authCustom header + SameSite
Mobile app with JWTNo CSRF protection needed

The safest approach is to combine SameSite cookies + CSRF tokens. Defense in depth.

Testing for CSRF

  1. Manual test: Open your app, intercept a POST request with browser DevTools, copy it, and replay it from a different origin. Does it succeed?
  2. Remove the CSRF token from a form submission. Does the server reject it?
  3. Use OWASP ZAP — it has built-in CSRF testing that scans forms automatically
  4. Check SameSite cookies — look at your cookies in DevTools and verify SameSite is set

Common Mistakes

  • Disabling CSRF protection for convenience — Django’s @csrf_exempt should be rare, not common
  • Using GET for state-changing actions — GET requests are not protected by SameSite=Lax
  • Not validating the token server-side — including a token in the form means nothing if you do not check it
  • SameSite=None without a reason — this disables the browser’s built-in CSRF protection

Prevention Checklist

DefensePriorityNotes
SameSite=Lax on session cookiesHighDefault in modern browsers, but set it explicitly
CSRF tokens for HTML formsHighUse framework built-in support (Django, Spring, Gorilla)
Custom headers for API requestsMediumX-Requested-With or X-CSRF-Token
Avoid GET for state changesHighGET requests should only read data
Double-submit cookie for SPAsMediumNo server-side session needed
JWT in Authorization headerHighEliminates CSRF entirely for API calls

What is Next?

In the next tutorial, you will learn about CORS (Cross-Origin Resource Sharing) — a closely related concept that controls which websites can make requests to your API. CSRF and CORS are often confused, and understanding both is essential for web security.