Guides8 min read17 October 2026

How to Fix a CSRF Vulnerability on Your Website

Cross-site request forgery tricks authenticated users into performing actions without their knowledge. This guide explains how CSRF works and the concrete steps to fix it.

By Yrzo AI — UK cybersecurity specialists

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.

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 →