Safeguard
Vulnerability Guides

What is CSRF (Cross-Site Request Forgery)?

CSRF makes a logged-in user's browser perform actions they never intended — changing an email, moving money, granting access — using the victim's own session. Here's how it works and how to stop it.

Daniel Osei
Security Researcher
Updated 5 min read

Your user is logged into your app in one tab. In another, they open an ordinary-looking page. Without a single click on your site, that page silently instructs their browser to change their account email to one the attacker controls. The request carries the victim's valid session cookie, your server sees a perfectly authenticated action, and the takeover is complete. That is cross-site request forgery.

What is CSRF, exactly?

Cross-Site Request Forgery (CSRF, CWE-352) is a vulnerability where an attacker causes a victim's authenticated browser to send a state-changing request to an application the victim is logged into, without the victim's intent. It exploits ambient authority: browsers automatically attach cookies (and other credentials) to requests toward a site, so a request forged from an attacker-controlled page still arrives fully authenticated. CSRF appeared in the OWASP Top 10 for years and remains a standard finding wherever cookie-based sessions meet unprotected state changes.

How the attack works

CSRF depends on three conditions: the action changes state, it relies on an automatically sent credential (typically a session cookie), and it has no unpredictable parameter the attacker cannot guess. When all three hold, an attacker can forge the request from any origin. A classic auto-submitting form:

<!-- hosted on evil.tld; fires as soon as the victim loads the page -->
<form action="https://bank.example/transfer" method="POST" id="f">
  <input type="hidden" name="to" value="attacker-account">
  <input type="hidden" name="amount" value="5000">
</form>
<script>document.getElementById("f").submit();</script>

The victim's browser attaches their bank.example session cookie automatically, and the transfer executes under their identity. GET-based endpoints are even easier to forge — a <img src="https://app.example/account/delete"> tag can trigger them with no script at all, which is exactly why state changes must never happen on GET.

Vulnerable vs. fixed

An Express route that changes the account email using only the session:

// VULNERABLE — trusts the session cookie alone; no anti-CSRF control
app.post("/account/email", (req, res) => {
  // browser sent the session cookie automatically, even from evil.tld
  updateEmail(req.session.userId, req.body.email);
  res.sendStatus(200);
});

The modern fix combines a SameSite cookie (which stops the browser from attaching the session on cross-site requests) with a synchronizer token the server validates on every state change:

// FIXED — SameSite cookie + validated per-session CSRF token
app.use(session({
  cookie: { httpOnly: true, secure: true, sameSite: "lax" }, // blocks cross-site sends
}));

app.post("/account/email", csrfProtection, (req, res) => {
  // csrfProtection rejects the request unless a valid, session-bound
  // token is present in a header/body field the attacker cannot read or guess
  updateEmail(req.session.userId, req.body.email);
  res.sendStatus(200);
});

SameSite=Lax (now the default in major browsers) already blocks the cross-site POST in most cases, and SameSite=Strict is stronger where usability allows. The synchronizer token is defense in depth: because the attacker's page cannot read the token out of your origin (the same-origin policy forbids it), it cannot include a valid one.

Prevention checklist

  • Set SameSite on session cookies. Lax is a sensible default; Strict for the most sensitive apps. This blocks the majority of CSRF at the browser.
  • Use anti-CSRF tokens for state-changing requests — synchronizer tokens or the double-submit-cookie pattern — bound to the session and unpredictable.
  • Never change state on GET. Reserve GET for reads; use POST/PUT/PATCH/DELETE for mutations.
  • For token-based (SPA/API) auth, prefer credentials that are not sent ambiently — an Authorization header set explicitly by your client is not attached automatically cross-site, which sidesteps classic CSRF (but re-check if you store tokens in cookies).
  • Verify Origin/Referer on sensitive endpoints as an additional signal.
  • Require re-authentication or step-up for the highest-impact actions (password, email, payment changes).
  • Add SameSite-aware CORS; a permissive CORS policy can undermine your other defenses.

CSRF vs. XSS: a common confusion

What is CSRF versus XSS? The two are frequently mixed up, but they are opposites in an important way. XSS is a failure to control your own output, letting an attacker run script inside your origin. CSRF requires no script on your site at all — it abuses the browser's willingness to attach credentials to requests your application receives. Crucially, an XSS bug defeats most CSRF defenses: script running in your origin can read anti-CSRF tokens and forge requests freely. That is why CSRF tokens are necessary but not sufficient, and why fixing XSS is effectively a prerequisite for trusting any CSRF control.

Why "it's an API, we're safe" is often wrong

Teams building single-page apps sometimes assume CSRF does not apply to JSON APIs. It depends entirely on how the API authenticates. If the browser sends a session or auth cookie automatically, the endpoint is CSRF-exploitable regardless of content type unless it validates a token or the request origin. Only credentials the browser does not attach ambiently — an Authorization header your client sets explicitly on each request — sidestep the problem by design.

How Safeguard helps

CSRF is fundamentally a missing-control problem, so it is best confirmed by exercising the application. Safeguard's DAST engine replays state-changing requests without valid tokens and from foreign origins to determine which endpoints actually accept forged requests — turning a checklist item into concrete evidence. Griffin AI code review inspects your routes and middleware to flag state-changing handlers that lack CSRF protection, cookies missing SameSite, and mutations exposed over GET, reasoning about your framework's conventions the way a reviewer would. When the fix is enabling CSRF middleware or hardening cookie flags, Safeguard's auto-fix can raise the pull request, and the Safeguard CLI surfaces the gaps before code reaches review.

Deciding between platforms? Start with the comparison hub or check pricing and plans.

Create your free account at app.safeguard.sh/register, and read the session-security guides at docs.safeguard.sh.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.