# CSRF (Cross Site Request Forgery) tutorial

> Learn how CSRF attacks make authenticated browsers send unwanted requests, and how tokens, SameSite cookies, and origin checks prevent them.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2023-05-12 | Updated: 2026-07-18 | Canonical: https://flaviocopes.com/csrf/

**CSRF** stands for **Cross Site Request Forgery**.

A CSRF attack tricks a person's browser into sending an unwanted request to a site where that person is already logged in.

The important detail is that browsers automatically attach credentials such as session cookies to some requests. Without a separate CSRF check, the server might not be able to tell a request from its own page from a request triggered by an attacker's page.

A successful attack can change an email address, delete data, make a purchase, or perform any other action available to the victim.

## What does a CSRF attack look like?

Imagine this endpoint deletes a car:

```text
https://example.com/api/delete?id=1
```

If it accepts a `GET` request, an attacker can put this image on another site:

```html
<img src="https://example.com/api/delete?id=1" alt="" />
```

The browser requests the URL while trying to load the image. Depending on the cookie settings, it may also send the victim's session cookie.

Never use `GET` for an operation that changes data. A `GET` request should be safe to repeat and should only retrieve data.

Using `POST` is necessary, but it is not a CSRF defense on its own. An attacker's page can submit a form:

```html
<form action="https://example.com/api/delete" method="post">
  <input type="hidden" name="id" value="1" />
</form>
<script>
  document.querySelector('form').submit()
</script>
```

The same risk applies to `PUT`, `PATCH`, and `DELETE` endpoints when the browser automatically sends authentication credentials.

## When is an application vulnerable?

CSRF is mainly a concern when authentication is attached automatically by the browser. Session cookies are the most common example. HTTP Basic authentication and client certificates can have the same property.

An API that only accepts a bearer token in an `Authorization` header is less exposed to classic CSRF because a random page cannot add that header. You still need to configure [CORS](https://flaviocopes.com/cors/) correctly and protect the token from [XSS](https://flaviocopes.com/xss/).

## Use the framework's CSRF protection

First check whether your framework has a maintained, built-in CSRF feature. It is easy to get the details of token generation and validation wrong.

Do not start a new implementation with the old Express `csurf` package. Its repository was archived in 2025 and it is no longer maintained.

## Use a synchronizer token

For a server-rendered application with sessions, the usual approach is the synchronizer token pattern:

1. Generate a secret, unpredictable token on the server.
2. Associate it with the user's session.
3. Put it in a hidden form field or send it to your JavaScript application.
4. Send it back in the form body or a custom request header.
5. Compare it with the session token on the server and reject the request if it is missing or wrong.

Here is the form part:

```html
<form action="/settings/email" method="post">
  <input type="email" name="email" />
  <input type="hidden" name="csrf_token" value="TOKEN_FROM_SERVER" />
  <button>Save</button>
</form>
```

Do not put a CSRF token in a URL. URLs can appear in browser history, server logs, analytics tools, and `Referer` headers.

For a single-page application, it is common to send the token in a custom header. If you use the double-submit cookie pattern, use a **signed token bound to the current session**. Comparing an unsigned cookie with an identical request value is vulnerable to cookie injection in some setups.

## Configure SameSite cookies

Set `SameSite` explicitly on session cookies:

- `SameSite=Strict` sends the cookie only in same-site contexts. It gives stronger protection but can make links from other sites appear to log the user out.
- `SameSite=Lax` allows some top-level navigation requests while blocking the cookie on many cross-site subrequests and form submissions.
- `SameSite=None` allows cross-site use and also requires `Secure`.

Also use `Secure` and `HttpOnly` for session cookies. Prefer a host-only cookie, or the `__Host-` cookie prefix when possible, instead of sharing a session cookie with every subdomain.

`SameSite` is a useful layer, but do not treat it as the only defense for important state-changing operations.

## Check where the request came from

The server can reject unsafe requests whose `Origin` does not match its own origin. When `Origin` is absent, a carefully implemented check can fall back to `Referer`.

Modern browsers also send Fetch Metadata headers. In particular, `Sec-Fetch-Site: cross-site` lets a server identify and reject an obvious cross-site request. Plan a fallback for clients that do not send these headers.

For JSON APIs, requiring a custom request header and rejecting simple form content types adds another barrier. This only works when CORS does not allow untrusted origins to send credentialed requests.

## Add confirmation to sensitive actions

For actions such as changing a password or sending money, ask the user to re-enter a password, use a passkey, or confirm with another authentication factor.

This verifies the user's intent and reduces the damage of several kinds of attacks. It is an extra layer, not a replacement for normal CSRF protection.

Finally, remember that XSS can bypass most CSRF defenses because malicious code running on your own origin can read tokens and send valid requests. You need to prevent both vulnerabilities.

The [OWASP CSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) has the detailed implementation guidance.
