Skip to content
cybersecurity attacks

Cross-Site Request Forgery (CSRF)

csrf web-security attacks cookies authentication
Plain English

CSRF is a forged letter sent in your name. Imagine you are logged into your bank and you visit a malicious website. That site secretly sends your browser a pre-filled transfer form to your bank. Because your browser automatically attaches your bank’s session cookie to every request, the bank thinks you submitted it. The attacker never saw your credentials; they just tricked your browser into using them for their purpose. The malicious site does not need to read the response, just make the request happen.

Technical Definition

Cross-Site Request Forgery (CSRF) exploits the browser’s behavior of automatically including cookies (including session cookies) on every request to the associated domain, regardless of which page initiated the request. An attacker crafts a request to a trusted site and tricks a victim’s browser into sending it while the victim is authenticated.

Attack mechanics:

  1. Victim logs into bank.com — browser stores a session cookie for bank.com
  2. Victim visits attacker-controlled evil.com
  3. evil.com contains a hidden form or <img> tag targeting bank.com/transfer
  4. Browser auto-submits the request with the valid session cookie
  5. bank.com processes the transfer as if the victim initiated it

Classic CSRF vector (HTML form auto-submit):

<!-- Hidden on evil.com, triggers on page load -->
<form action="https://bank.com/transfer" method="POST" id="csrf">
  <input type="hidden" name="to_account" value="attacker_account">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById("csrf").submit();</script>

Defenses:

DefenseMechanismNotes
CSRF tokenServer issues a random secret per session; form must include itAttacker cannot read the token due to SOP
SameSite=Strict cookieBrowser omits cookie on cross-site requests entirelyBest defense; breaks some OAuth flows
SameSite=Lax cookieCookie sent on top-level navigations, not sub-requestsGood default; Set-Cookie: session=...; SameSite=Lax
Double submit cookieToken in both cookie and request body; server compares themStateless alternative to server-side token storage
Origin/Referer checkServer rejects requests where Origin header is not its own domainDefense in depth; not sufficient alone
Custom request headerRequire X-Requested-With: XMLHttpRequest headerSOP prevents cross-origin scripts from setting custom headers

What CSRF is not:

  • CSRF does not steal data (the attacker’s site cannot read responses from the target due to the Same-Origin Policy)
  • CSRF only affects state-changing actions (POST, PUT, DELETE, PATCH). GET requests must never have side effects
  • CSRF is distinct from XSS: XSS injects code into the trusted site; CSRF exploits trust in the browser

CSRF token implementation and SameSite cookie configuration

// Express.js: CSRF token middleware with csurf or manual implementation
import crypto from "crypto";

// Generate a CSRF token and store in session
export function generateCsrfToken(session: Session): string {
  const token = crypto.randomBytes(32).toString("hex");
  session.csrfToken = token;
  return token;
}

// Validate incoming token against session
export function verifyCsrfToken(session: Session, submittedToken: string): boolean {
  if (!session.csrfToken || !submittedToken) return false;
  // Constant-time comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(session.csrfToken, "hex"),
    Buffer.from(submittedToken, "hex")
  );
}

// Middleware: enforce CSRF on state-changing routes
export function csrfProtect(req: Request, res: Response, next: NextFunction): void {
  if (["GET", "HEAD", "OPTIONS"].includes(req.method)) { next(); return; }
  const token = req.body._csrf || req.headers["x-csrf-token"];
  if (!verifyCsrfToken(req.session, token as string)) {
    res.status(403).json({ error: "Invalid CSRF token" });
    return;
  }
  next();
}
# Set session cookie with SameSite and HttpOnly (Nginx)
# In application code (Node.js / Express):
# res.cookie("session", value, {
#   httpOnly: true,
#   secure: true,
#   sameSite: "strict"   # or "lax" for OAuth compatibility
# })

# Verify cookie attributes on a live response
$ curl -sI -c /dev/null https://example.com/login \
  | grep -i set-cookie
set-cookie: session=...; Path=/; HttpOnly; Secure; SameSite=Strict
In the Wild

CSRF was one of the most common web vulnerabilities through the early 2010s. The introduction of SameSite cookies (Chrome in 2020 made SameSite=Lax the default for cookies without an explicit attribute) significantly reduced CSRF risk for modern browsers. Django and Rails have built-in CSRF middleware enabled by default. Spring Security requires an explicit opt-out. The remaining risk is legacy applications that rely solely on session cookies without SameSite, and REST APIs that accept application/x-www-form-urlencoded or multipart/form-data with simple CORS policies. Single-Page Applications using JWTs in Authorization headers (not cookies) are naturally CSRF-immune since the attacker cannot set custom headers cross-origin. For bank-level security, use SameSite=Strict, CSRF tokens, and verify the Origin header as three independent layers.