Mesrai
Back to blog
// essaySecurity

Missing CSRF Token on State-Changing Endpoints

Real PR catch: POST endpoint with no CSRF guard. Why SameSite is not enough, the attacker form, and the double-submit fix. CWE-352.

Mesrai TeamJuly 24, 20268 min read

A state-changing endpoint that updates the user's email. Auth required, no other guard. Mesrai's review comment flagged the missing CSRF token check.

The vulnerable diff

tsapi/profile.ts
// api/profile.ts
app.post("/profile/email", requireAuth, async (req, res) => {
  // BUG: no CSRF check. requireAuth only verifies the session cookie.
  await db.users.update(req.user.id, { email: req.body.email });
  res.json({ ok: true });
});

What is wrong

CSRF works because browsers automatically attach cookies to cross-site requests (subject to the SameSite flag). An attacker page that issues a POST to your endpoint runs in the victim's authenticated context — the request looks identical to a legitimate one because the browser supplied the session cookie unprompted. SameSite cookies (especially `lax`) prevent most CSRF, but not all: top-level navigation POSTs in some browsers, subdomain confusion, and cookieless auth schemes (Bearer headers in localStorage) still need explicit CSRF protection. CWE-352 is the class.

The attack

Attacker page hosted on evil.com:

htmlexploit
<!-- evil.com/landing-page.html -->
<form action="https://app.example.com/profile/email" method="POST" id="f">
  <input name="email" value="attacker@evil.com">
</form>
<script>
  document.getElementById("f").submit();
  // Victim browser auto-attaches app.example cookie if sameSite isn't strict.
  // Email gets changed without victim's knowledge.
</script>

Next step: attacker triggers a password reset to the new email — full account takeover.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 1 min ago

[mesrai] [code-review] [Security] [CWE-352] [high]

State-changing POST endpoint without CSRF token validation. The
auth middleware only checks the session cookie — which the browser
supplies on cross-site POSTs by default.

Two options for 2026:

  1. (Recommended) sameSite:strict cookies + double-submit token
     in a custom header. The header is set by your frontend JS;
     attacker JS on another origin cannot set custom headers on
     cross-origin requests without preflight.

  2. Bearer token in Authorization header from localStorage. No
     cookie means no automatic browser-attached credential —
     CSRF is structurally impossible. (Trade-off: more XSS surface.)

For cookie-based sessions, add the csrfProtection middleware:

  app.post("/profile/email", requireAuth, csrfProtection, async ...

And ensure the frontend reads the token from a Set-Cookie or a
meta tag and sends it back in `X-CSRF-Token`.

Reference: OWASP CSRF Prevention Cheat Sheet, CWE-352

The fix

tsapi/profile.ts (fixed)
// api/profile.ts — fixed
import { csrfProtection } from "../middleware/csrf";

app.post("/profile/email", requireAuth, csrfProtection, async (req, res) => {
  await db.users.update(req.user.id, { email: req.body.email });
  res.json({ ok: true });
});

`csrfProtection` validates a token in a custom header against a value bound to the session. Since cross-origin attacker JS cannot set custom headers without a CORS preflight (and your endpoint can reject preflights), the token cannot be replayed from another origin. Combine with `sameSite:strict` cookies for defense in depth.

Why human review missed it

CSRF used to be on every checklist a decade ago, then SameSite cookies arrived and many teams concluded the class was closed. It is not — Lax leaves top-level navigation POSTs unprotected, Strict breaks OAuth flows, and `Bearer` token APIs are a separate consideration. Mesrai's rule pack flags every state-changing endpoint without an explicit CSRF check, regardless of the cookie config.

Related rules + further reading

Mesrai rule pack: security/csrf-required-on-mutations — flags POST/PUT/PATCH/DELETE handlers without CSRF middleware.

OWASP: Cross-Site Request Forgery Prevention Cheat Sheet.

CWE-352 — Cross-Site Request Forgery.

Takeaway

SameSite cookies help. They do not close the CSRF class. Pair with a double-submit token on every state-changing endpoint.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.