Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

CSRF Attack and Defense Practices

I wrote about XSS defense before; this time let's talk about CSRF. They are often mentioned together, but the underlying principles are completely different.

What is CSRF ​

CSRF (Cross-Site Request Forgery): an attacker tricks a logged-in user into visiting a malicious page, which then makes requests on the user's behalf.

Normal flow: User → logs into bank website (gets cookie) → transfers money

CSRF attack:
User → logs into bank (has cookie)
     → visits attacker's page (contains <img src="https://bank.com/transfer?to=hacker&amount=10000">)
     → browser automatically sends the request with the bank's cookie
     → bank executes the transfer!

Key point: the browser automatically includes the target domain's cookie when making requests — the attacker never needs to know the cookie value.

Defense Strategies ​

1. CSRF Token (Most Common) ​

The server generates a random token, embeds it in forms/request headers, and the attacker cannot forge it:

javascript
// Server: generate token and store in session
const csrfToken = crypto.randomBytes(32).toString("hex");
req.session.csrfToken = csrfToken;

// Return it to the frontend (in a response header or meta tag)
res.setHeader("X-CSRF-Token", csrfToken);
javascript
// Frontend: include the token in every request
// axios global config
axios.interceptors.request.use((config) => {
  const token = document.querySelector('meta[name="csrf-token"]')?.content;
  if (token) {
    config.headers["X-CSRF-Token"] = token;
  }
  return config;
});
javascript
// Server: validate the token
app.use((req, res, next) => {
  if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
    return next(); // read-only requests don't need validation
  }

  const token = req.headers["x-csrf-token"];
  if (!token || token !== req.session.csrfToken) {
    return res.status(403).json({ error: "CSRF token invalid" });
  }
  next();
});

Modern browsers support the SameSite attribute to control whether cookies are sent with cross-site requests:

Set-Cookie: session=xxx; SameSite=Strict; HttpOnly; Secure
  • Strict: cookies are never sent with cross-site requests
  • Lax: allows cross-site navigation (link clicks), not form/fetch requests (Chrome 80+ default)
  • None: allows cross-site (must also set Secure)
javascript
// Express cookie settings
res.cookie("sessionId", token, {
  httpOnly: true,
  secure: true,
  sameSite: "lax", // recommend "strict" in production
});

3. Referer/Origin Validation ​

Check the request's origin domain:

javascript
app.use((req, res, next) => {
  const origin = req.headers.origin || req.headers.referer;
  if (origin && !origin.startsWith("https://yourdomain.com")) {
    return res.status(403).json({ error: "Invalid origin" });
  }
  next();
});

Downside: the Referer header may be stripped by users or proxies, making this unreliable on its own.

CSRF vs XSS ​

CSRFXSS
ExploitsUser's identity (cookie)Browser executing attacker's code
Attacker canMake requests as the userRead cookies, alter pages, send requests
Core defenseToken + SameSite CookieCSP + input escaping

Summary ​

  • CSRF exploits the browser's automatic cookie behavior to make requests without the user's knowledge
  • CSRF Token is the most universal defense
  • SameSite: Lax/Strict is an effective defense in modern browsers
  • GET requests should have no side effects (otherwise an img src can be used to attack)

MIT Licensed