Safeguard
AppSec

What Is a CSRF Attack? Detection and Prevention Guide

A CSRF attack tricks a logged-in user's browser into sending forged requests. Here is how the attack works and how to shut it down with modern defenses.

Aisha Rahman
Security Analyst
7 min read

A CSRF attack (cross-site request forgery) forces an authenticated user's browser to send a request they never intended, using the credentials the browser already holds. Because the browser automatically attaches session cookies to any request going to a site, an attacker who can get your logged-in user to load a malicious page can make that page fire a state-changing request at your application, and your server sees a perfectly valid, authenticated call. This guide explains the mechanism, walks through a concrete csrf attack example, and then focuses on what actually stops it.

CSRF has been on the OWASP radar for two decades. It fell out of the Top 10 as a standalone entry once frameworks started shipping defenses by default, but it still shows up constantly in code that rolls its own session handling or exposes state-changing endpoints over simple GET requests.

How a CSRF attack works

The attack depends on three conditions holding at the same time:

  1. The victim is authenticated to the target site (a valid session cookie exists).
  2. The target performs a state-changing action based purely on that cookie, with no unpredictable per-request secret.
  3. The attacker can get the victim to load attacker-controlled content while that session is live.

If all three hold, the attacker's page can issue a request to the target. The browser attaches the session cookie automatically, and the server cannot tell the forged request from a legitimate one, because at the HTTP level it looks identical.

A CSRF attack example

Suppose a banking app changes the account email with a form POST to /account/email, authenticated only by the session cookie. An attacker hosts a page that a victim visits while logged in:

<!-- attacker-controlled page, shown for illustration -->
<form action="https://bank.example/account/email" method="POST" id="f">
  <input type="hidden" name="email" value="attacker@evil.example" />
</form>
<script>document.getElementById('f').submit();</script>

When the victim loads this page, the browser submits the form to the bank with the victim's session cookie attached. If the endpoint has no anti-CSRF protection, the email on the account changes, which the attacker can then use for a password reset. This is the canonical example of csrf attack behavior: no credential theft, no XSS, just abuse of the browser's willingness to send cookies cross-site.

An even weaker design uses GET for state changes. Then the attacker does not even need a form. A hidden image tag pointing at https://bank.example/transfer?to=attacker&amount=1000 fires the request when the page renders. Never let GET requests change state; that alone removes a whole class of trivial CSRF.

Defense 1: SameSite cookies

The single most effective modern mitigation is the SameSite cookie attribute. It tells the browser not to send the cookie on cross-site requests.

Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax

SameSite=Lax (the default in current Chromium and Firefox for cookies that do not set the attribute) blocks the cookie on cross-site POSTs and subresource requests, while still allowing top-level navigations. SameSite=Strict is tighter but breaks some legitimate cross-site navigation flows. For sensitive session cookies, Lax is the practical baseline and Strict is worth it where UX allows. Do not rely on browser defaults alone; set the attribute explicitly so behavior is consistent across clients.

Defense 2: anti-CSRF tokens

The classic defense is the synchronizer token pattern: the server issues an unpredictable token, embeds it in each form, and rejects any state-changing request whose token does not match the session. Because the attacker's cross-site page cannot read the token (the same-origin policy prevents it), it cannot forge a valid request.

Most frameworks do this for you. Django provides {% csrf_token %} and its middleware; Rails has protect_from_forgery; Spring Security enables CSRF tokens by default for browser clients. The failure mode is almost always a developer disabling the built-in protection to make an API "just work," so audit for that first.

For single-page apps, the double-submit cookie pattern is common: the server sets a random value in a cookie and the JavaScript reads it and echoes it in a custom header. The server checks that the header and cookie match. It works because attacker pages cannot set a custom header cross-site without a CORS preflight the server controls.

Defense 3: verify Origin and Referer

State-changing endpoints should check the Origin header (and fall back to Referer) against an allowlist of expected origins. Browsers set Origin on cross-origin requests and it cannot be spoofed by page JavaScript. This is a cheap, defense-in-depth layer that catches forged requests even if a token check is misconfigured.

ALLOWED = {"https://app.example"}

def is_same_origin(request):
    origin = request.headers.get("Origin")
    return origin in ALLOWED if origin else False

Where CSRF hides in modern stacks

Teams that build APIs and SPAs sometimes assume CSRF does not apply to them. It does whenever the browser authenticates automatically. Watch these spots:

  • Cookie-authenticated APIs. If your SPA uses a session cookie rather than a bearer token in an Authorization header, you are exposed and need token or SameSite protection. Bearer tokens in headers are not auto-attached, which is why they sidestep classic CSRF.
  • CORS misconfiguration. A permissive Access-Control-Allow-Origin combined with Allow-Credentials: true can undermine your other defenses. Review the OWASP guidance on request forgery when you configure CORS.
  • Login and logout CSRF. Forging a login can silently sign a victim into an attacker-controlled account; protect authentication endpoints too, not just post-login actions.

Detecting these gaps at scale is a job for automated application security testing. A DAST scanner can crawl authenticated flows and flag state-changing endpoints that accept requests without a valid token or origin check, which is far more reliable than manual review of every form. Pairing that with developer training in something like the Safeguard Academy closes the loop between finding the issue and not reintroducing it.

A minimal defense stack

For most applications, layering these gives strong protection:

  1. SameSite=Lax (or Strict) plus Secure and HttpOnly on session cookies.
  2. Framework CSRF tokens enabled on all state-changing routes, never disabled for convenience.
  3. Origin/Referer validation as defense in depth.
  4. No state changes over GET, ever.

Each layer covers a gap in the others. SameSite depends on browser behavior; tokens cover older clients and edge cases; origin checks catch token misconfiguration.

FAQ

What is the difference between CSRF and XSS?

XSS runs attacker JavaScript inside your site's origin, which lets it read tokens and do almost anything. CSRF runs no code inside your origin; it just tricks the browser into sending a request with existing cookies. A site with XSS cannot be protected by CSRF tokens, because the injected script can read the token, so fix XSS first.

Does SameSite=Lax fully prevent CSRF?

It stops the most common cross-site POST and subresource attacks, but it is not a complete substitute for tokens. Top-level GET navigations still send Lax cookies, so combine SameSite with anti-CSRF tokens and safe HTTP methods rather than relying on one control.

Are REST APIs vulnerable to CSRF?

Only when the browser authenticates them automatically, which means cookie-based sessions. APIs that authenticate with a bearer token placed in the Authorization header by JavaScript are not exposed to classic CSRF, because that header is not attached automatically on cross-site requests.

How do I test for CSRF?

Manually, try replaying a state-changing request from a different origin without a token and see if it succeeds. At scale, use a DAST tool that authenticates and probes each state-changing endpoint for missing token or origin validation, then track fixes so regressions are caught in CI.

Never miss an update

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