Cross-Site Request Forgery (CSRF)
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.
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:
- Victim logs into
bank.com— browser stores a session cookie forbank.com - Victim visits attacker-controlled
evil.com evil.comcontains a hidden form or<img>tag targetingbank.com/transfer- Browser auto-submits the request with the valid session cookie
bank.comprocesses 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:
| Defense | Mechanism | Notes |
|---|---|---|
| CSRF token | Server issues a random secret per session; form must include it | Attacker cannot read the token due to SOP |
| SameSite=Strict cookie | Browser omits cookie on cross-site requests entirely | Best defense; breaks some OAuth flows |
| SameSite=Lax cookie | Cookie sent on top-level navigations, not sub-requests | Good default; Set-Cookie: session=...; SameSite=Lax |
| Double submit cookie | Token in both cookie and request body; server compares them | Stateless alternative to server-side token storage |
| Origin/Referer check | Server rejects requests where Origin header is not its own domain | Defense in depth; not sufficient alone |
| Custom request header | Require X-Requested-With: XMLHttpRequest header | SOP 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 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.