What CSRF Actually Does
Cross-site request forgery (CSRF) exploits the way browsers automatically attach cookies to requests. When a user is logged into your application, their browser holds a session cookie. When a request is made to your application — whether the user initiated it or not — the browser sends that cookie along automatically.
CSRF attacks work by luring an authenticated user to a page controlled by an attacker. That page contains a hidden form or an image tag that silently makes a request to your application. Because the user's browser sends the session cookie with the request, your application sees it as a legitimate authenticated action.
The classic example: a banking application has a funds transfer endpoint at `POST /transfer` that accepts a destination account number and an amount. A user is logged into their bank. An attacker sends them a phishing email containing a link to a page with this hidden form:
```html <form action="https://bank.example.com/transfer" method="POST"> <input type="hidden" name="to" value="attacker-account"> <input type="hidden" name="amount" value="5000"> </form> <script>document.forms[0].submit();</script> ```
When the user visits the page, the form submits silently. The bank receives a POST request with a valid session cookie and executes the transfer.
Step 1: Implement CSRF Tokens
The primary defence against CSRF is a synchronised token. The server generates a unique, unpredictable token for each user session (or per-request for higher security), embeds it in every form, and validates it on every state-changing request.
The token must be: - **Unpredictable** — cryptographically random, not derived from the session ID or any guessable value - **Tied to the user session** — the server must validate that the token belongs to the session making the request - **Present in every state-changing request** — GET requests should never change state; all POST, PUT, PATCH, and DELETE requests should require the token
**In a server-rendered application**, embed the token as a hidden field in every form:
```html <form method="POST" action="/update-email"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="email" name="email"> <button type="submit">Update</button> </form> ```
**In a single-page application making API calls**, the common pattern is the Double Submit Cookie method: the server sets a CSRF token as a cookie, the JavaScript reads that cookie and includes its value in a custom request header (for example, `X-CSRF-Token`), and the server validates that the header value matches the cookie value. Because an attacker on a different origin cannot read your cookies, they cannot set the correct header value.
**Validation on the server side:**
```python def validate_csrf_token(request): token_from_form = request.form.get('csrf_token') token_from_session = session.get('csrf_token') if not token_from_form or not token_from_session: abort(403) if not hmac.compare_digest(token_from_form, token_from_session): abort(403) ```
Use `hmac.compare_digest` or an equivalent constant-time comparison function to prevent timing attacks.
Step 2: Set the SameSite Cookie Attribute
The SameSite cookie attribute is a defence-in-depth measure that modern browsers support. It instructs the browser on when to send the cookie with cross-site requests.
``` Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly ```
**SameSite=Strict** — The cookie is never sent with cross-site requests. This provides the strongest CSRF protection but can cause issues with links from external sites (the user arriving via a link from another site will not be logged in until they navigate to another page).
**SameSite=Lax** — The cookie is sent with top-level GET navigations (clicking a link) but not with cross-origin POST, PUT, or DELETE requests. This is the browser default in most modern browsers and provides meaningful CSRF protection for POST-based attacks. It does not protect against GET-based state changes (a further reason to never use GET for state-changing operations).
**SameSite=None** — The cookie is sent with all cross-site requests. Required only for cookies used in cross-site contexts (for example, a third-party widget that needs to authenticate). Must be used with `Secure`.
Set SameSite=Strict or SameSite=Lax on all session cookies. This is not a complete CSRF defence on its own because older browsers do not support the attribute, but it is an important layer.
Step 3: Validate the Origin Header
For API endpoints, check the `Origin` or `Referer` header of incoming requests. Legitimate requests from your own application will have an Origin matching your domain. Requests forged from a third-party site will have a different Origin.
```python def check_origin(request): origin = request.headers.get('Origin') if origin and origin != 'https://yourapp.example.com': abort(403) ```
This is not a substitute for CSRF tokens — Origin headers can be absent in some scenarios — but it adds a useful layer.
Step 4: Use Your Framework's Built-In Protection
Most web frameworks include CSRF protection that handles token generation and validation automatically. Use it rather than implementing your own:
- **Django** — CSRF middleware is enabled by default. Use `{% csrf_token %}` in forms. - **Rails** — `protect_from_forgery` is enabled by default in ApplicationController. - **Laravel** — CSRF middleware is included. Use `@csrf` in Blade forms. - **Express (Node.js)** — Use the `csurf` package or a more modern alternative. - **Spring Security** — CSRF protection is enabled by default.
The most common cause of CSRF vulnerabilities in applications using these frameworks is explicitly disabling the built-in protection — sometimes to fix an unrelated issue, and then never re-enabling it.
Step 5: Never Use GET for State-Changing Operations
GET requests should retrieve data, never change it. If your application changes state on a GET request — deletes a record, updates a setting, processes a payment — any link an attacker can embed in an external page (including an image tag with `src="https://yourapp.com/delete?id=123"`) can trigger that action.
Review your routing and ensure all state-changing endpoints require POST, PUT, PATCH, or DELETE.
CSRF is one of several attack types that target authenticated user sessions. [Session hijacking](/blog/session-hijacking-attack-explained) is the complementary threat — where CSRF abuses an active session, session hijacking steals the session token itself. Both require attention in any web application security review. If you're also looking at injection vulnerabilities, [what is SQL injection](/blog/what-is-sql-injection) and [what is XML injection](/blog/what-is-xml-injection) cover similar input-handling problems with different attack vectors.
Yrzo AI's automated security assessment checks for CSRF protection across your web application's forms and state-changing endpoints. Findings include the specific endpoint and remediation steps. From yrzoai.dev.
Find out if your website has these vulnerabilities
Yrzo AI runs 44 automated security checks and delivers a full report in under 20 minutes. Starting from £149.
Scan your website →