Safeguard
Security

What Is a Cross-Site Request Forgery Vulnerability?

A cross-site request forgery vulnerability tricks a logged-in user's browser into sending unwanted requests. Here is how it works and how to shut it down.

Priya Mehta
Security Analyst
6 min read

A cross-site request forgery vulnerability lets an attacker force a victim's authenticated browser to submit a request the victim never intended to make. The classic name is CSRF (sometimes written XSRF), and it exploits one simple fact: browsers attach cookies to every request to a domain automatically, whether the request came from your own page or from an attacker's.

If a user is logged in to bank.example and then visits a malicious page, that page can quietly issue a request to bank.example — a form post, an image load, a fetch call — and the browser will helpfully include the session cookie. The server sees a valid session and processes the action. The user approved nothing.

How the attack actually works

CSRF only matters when three conditions hold at once. The application uses a cookie (or HTTP Basic auth, or any ambient credential) that the browser sends automatically. The action being triggered changes state — transferring money, changing an email address, deleting a record. And the request has no unpredictable parameter the attacker cannot guess.

Here is a minimal example of a hostile page. The victim only has to load it:

<body onload="document.forms[0].submit()">
  <form action="https://bank.example/transfer" method="POST">
    <input type="hidden" name="to" value="attacker-account" />
    <input type="hidden" name="amount" value="5000" />
  </form>
</body>

No JavaScript is even required for the request itself — an auto-submitting form or a pre-filled <img> tag works because the browser sends the cookie regardless of where the request originated. That is the whole trick. The attacker never sees the response and never steals the cookie; they simply borrow the victim's authenticated context.

GET-based CSRF is the easiest to pull off because you can embed the URL in an image or a link. That is one reason state-changing operations should never happen on a GET request.

Anti-CSRF tokens: the primary defense

The standard defense is the synchronizer token pattern. The server generates a random, unpredictable value tied to the user's session, embeds it in every form, and rejects any state-changing request that does not echo it back. An attacker's page cannot read that token because the same-origin policy blocks it from reading your page's DOM.

<form action="/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="a7f3c9e1b2d84f60..." />
  <!-- other fields -->
</form>

On the server side, validation is a constant-time comparison:

def validate_csrf(request, session):
    submitted = request.form.get("csrf_token", "")
    expected = session.get("csrf_token", "")
    if not expected or not hmac.compare_digest(submitted, expected):
        abort(403)

Most mature frameworks ship this out of the box: Django's {% csrf_token %}, Rails' protect_from_forgery, Spring Security's CsrfFilter, and Laravel's @csrf directive. The failure mode is almost always a developer disabling the protection to make an API "just work" and never turning it back on.

SameSite cookies: defense in depth

The SameSite cookie attribute is a browser-level control that limits when cookies ride along on cross-site requests. SameSite=Lax, now the default in modern browsers, withholds the cookie on cross-site POST requests and subresource loads while still allowing it on top-level navigations. SameSite=Strict withholds it on all cross-site requests.

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

SameSite is genuinely useful, but treat it as a second layer rather than your only one. Older clients may not enforce it, and certain request shapes still slip through. Pair it with tokens rather than choosing between them.

Cross-site request forgery testing

Good cross-site request forgery testing starts with a simple question for every state-changing endpoint: what stops a request that originates from another origin? Walk your routes and check three things.

First, confirm that GET requests never mutate state. Second, capture a legitimate request in a proxy such as Burp Suite or OWASP ZAP, strip the CSRF token, replay it, and verify the server returns a 403 rather than processing it. Third, remove the Origin and Referer headers and replay again — some apps rely on header checks that are trivially bypassed when the header is simply absent.

A quick manual check with curl demonstrates the token requirement:

# Should be rejected: valid session cookie, no CSRF token
curl -X POST https://app.example/account/email \
  -H "Cookie: session=VALID_SESSION" \
  -d "email=attacker@evil.example" -i
# Expect: HTTP/1.1 403 Forbidden

For coverage at scale, fold cross-site request forgery testing into your dynamic scans so new endpoints are checked as they ship. A DAST scanner can crawl the running app and flag state-changing requests that accept a forged origin, which catches the endpoints a manual pass misses.

Common mistakes that reintroduce the flaw

Teams reopen this hole in predictable ways. They exempt an entire API prefix from CSRF protection to support a mobile client, then reuse cookie auth for the web app on the same routes. They validate the token only on some HTTP methods. They store the token in a cookie and read it back from the same cookie without any header echo, which defeats the point. Or they check the Referer header but allow the request when the header is missing.

Token-based auth via an Authorization: Bearer header is genuinely not vulnerable to classic CSRF, because the browser does not attach that header automatically. But the moment you fall back to cookies for convenience, the exposure returns.

Where CSRF sits among API risks

CSRF is a browser-and-cookie problem, so it is most acute for traditional server-rendered apps and cookie-authenticated single-page apps. Pure APIs consumed by non-browser clients with bearer tokens have a different threat model. If you are auditing API endpoints, map your findings against the broader authorization risks catalogued in the OWASP API Security Top 10, since a missing CSRF control often travels alongside weak authorization checks.

FAQ

Is a cross-site request forgery vulnerability still relevant with SameSite cookies?

Yes. SameSite=Lax as a browser default has reduced the blast radius, but it is not universal across all clients and request types. Anti-CSRF tokens remain the reliable primary control, with SameSite as defense in depth.

What is the difference between CSRF and XSS?

XSS runs attacker-controlled script inside your origin and can read your DOM, including CSRF tokens. CSRF never runs in your origin and cannot read responses; it only forces a request. An XSS flaw defeats CSRF protection entirely, which is why you fix both.

Do REST APIs need CSRF protection?

Only if they authenticate with cookies. APIs that require a bearer token in an Authorization header are not exposed to classic CSRF, because browsers do not attach that header automatically to cross-site requests.

How do I test for CSRF quickly?

Capture a legitimate state-changing request, remove the CSRF token (and separately the Origin/Referer headers), replay both, and confirm the server rejects them with a 403. Automate this across endpoints with a dynamic scanner so new routes are covered.

Never miss an update

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