What is CSRF Protection?

Stop Attackers From Forging Requests As Your Users

A user can be logged into your site in one tab and visit a trap page in another. The trap page can quietly send requests to your site using that user's session, and your server has no easy way to tell those requests apart from real ones. That is CSRF, and a tiny secret solves it.

7 min read Updated 2026-04-18 By Hasan

Why Would Anyone Trust a Request They Did Not Send?

Think of CSRF protection like the numbered stub a valet hands you when you drop off your car. You give them your keys; they give you a little paper stub with a number on it. To get the car back, you must hand that exact stub back. Someone else can spot your car in the lot, even know your name, but without the matching stub, the valet will not move it. The stub is worthless to them because it was issued to you, for this trip, and only the valet has the other half. CSRF protection works the same way: when your site loads a form for a logged-in user, the server also hands out a random, short-lived token. When that form comes back, the server checks the token. A real submission from your own page has it. A forged submission from somewhere else does not.
Without CSRF protection: a user logs into your banking app in one tab, then opens an innocent-looking page in another. That second page quietly contains a hidden form pointed at your bank, and a bit of JavaScript that auto-submits it. The browser sees the request is going to your bank, so it helpfully attaches the user's session cookie, just like it would for any other request. Your server sees a valid session, a well-formed POST, and executes a transfer. Nothing on your side looks wrong. Session cookies ride along with every request to their domain, even when the request was triggered by a page you did not write.
With CSRF protection: every form your server renders is stamped with a fresh, random token tied to the user's session. On submit, the server compares the token that came in with the one it issued. Real pages always have the right token because your own server just printed it there. An attacker's trap page cannot read that token (browsers block cross-site reads of your HTML), so the forged request arrives with no token or the wrong one. The server rejects it with a 403 and the fake transfer never happens. The token is proof the request actually came from a page your site rendered, not from a stranger's page borrowing the user's cookies.
TL;DR

CSRF Protection = a secret token your site issues with each form so the server can tell a real submission from one an attacker tricked the browser into sending.

Note

Most frameworks ship CSRF protection as middleware that is on by default. In Django it is CsrfViewMiddleware plus the {% csrf_token %} template tag. In Express you add the csurf (or newer csrf-csrf) package. Ask your AI: "is CSRF protection turned on in my [framework] project, and are my forms including the token?" You almost never write this code by hand.

When to Use CSRF Protection

CSRF Protection isn't always the right call. Here's a quick mental model:

Any endpoint that changes state using session cookies

POST, PUT, PATCH, DELETE routes that trust a user's session cookie to authorize the action. Transfers, profile edits, password changes, posting comments, placing orders: all need CSRF protection.

You have server-rendered forms behind a login

Classic Django, Rails, or Laravel apps render HTML forms after a user logs in. These are the exact shape CSRF was designed for. Include the token in every form and let middleware verify it on submit.

Your frontend and backend share a cookie-based session

If a logged-in React or Vue app talks to your API through a session cookie (not a bearer token in the Authorization header), CSRF applies to your API too. Use the double-submit cookie pattern: server sends the token as a readable cookie, JS reads it and echoes it in a request header.

Pure read-only GET endpoints

CSRF protects against state changes. A GET that only returns data does not need a token, provided it truly is read-only. Do not perform deletes or updates inside a GET handler, some ancient code does, and CSRF cannot save you there.

Stateless APIs authenticated by bearer token in a header

If clients send an Authorization: Bearer ... header (mobile apps, server-to-server, most modern public APIs), no cookie rides along automatically, so there is no forgery to block. CSRF only bites when the browser is attaching credentials for you.

Interactive CSRF Protection Demo

Submit a real form and then simulate an attacker's forged POST from another site. Watch the server accept the first and reject the second, and see exactly which step the token check lives in.

CSRF Protection Simulator

Simulated — no real calls
Origin:
Session cookie:
CSRF token sent:
Server expects:
Pick a scenario to send a request.
What to notice:
  • Notice the session cookie rides along with BOTH requests, it is the token that makes the difference
  • The attacker's request has no CSRF token, the server rejects it with 403 before any logic runs
  • The token is random per session, refresh and it changes, attackers cannot guess it

AI Prompts for CSRF Protection

Now that you understand csrf protection, use these prompts with your AI coding agent. Copy the one that matches what you're building — the agent will handle the implementation.

Tip: These prompts work with any AI (ChatGPT, Claude, Cursor, Copilot). Just copy, paste, and fill in the [brackets]. You do not need to know the CSRF details, the AI does.

Add CSRF protection to my [Django / Rails / Laravel / Express] app and show me the form changes I need to make. I want you to: 1. Confirm whether CSRF middleware is already enabled by default in my framework 2. Show me the exact line(s) I need to add to each HTML form to include the token 3. Explain what happens on submit when the token is missing or wrong (what status code, what error message) 4. Tell me how to test it: one request with the token (should succeed), one without (should get rejected) My stack: [your framework and version here] Form page I want to protect: [path or URL] I am learning, so explain each part simply.
starter Start here. Turning protection on for server-rendered forms is usually a two-line change
My [React / Vue / plain JS] frontend makes AJAX requests to my [Django / Rails / Express] backend using session cookies. Set up CSRF protection for these requests. Walk me through: 1. The double-submit cookie pattern: server sets a CSRF cookie, frontend reads it and sends it back in a header like X-CSRF-Token 2. Exactly where to set the cookie on the backend and how to read it in the frontend 3. Which fetch/axios interceptor to add so I do not have to remember the header on every call 4. What the server should do when the header is missing or does not match (reject with 403) My stack: [your backend and frontend frameworks here] Show working code for one endpoint end to end. I am learning, so explain each part simply.
intermediate For SPAs that still use session cookies, this is the standard pattern
I already have CSRF tokens turned on. Now I want to harden my session cookies with the SameSite attribute as defense in depth. Explain and then change my config so that: 1. My session cookie is set with SameSite=Lax (or Strict, and you tell me which fits my app) 2. I understand what each value means: None / Lax / Strict, and what breaks with each 3. I know which cross-origin requests will stop working after the change (for example, logins via an external link) 4. My cookie is also marked Secure and HttpOnly where appropriate My stack: [your framework here] Do I have any third-party sites that POST to me legitimately? [yes/no, details] I am learning, so explain each part simply and call out any surprise that might break users.
advanced SameSite on session cookies blocks the browser from sending them on cross-site requests, belt and braces with CSRF tokens
Review my app for endpoints that change state but might be missing CSRF protection. Here is my route list / URL config: [paste your routes, urls.py, or route file here] For each endpoint, tell me: 1. Does it change state (POST, PUT, PATCH, DELETE, or a state-changing GET)? 2. Does it rely on a session cookie for auth? (If yes, CSRF applies. If it is a bearer-token API only, CSRF does not apply.) 3. Is CSRF middleware covering it? Or has someone @csrf_exempt-ed it? (flag every exemption) 4. For endpoints that should be protected but are not, show me exactly what to add. Framework: [your framework here] I am learning, so explain each finding simply, and be blunt about anything that looks risky.
documentation Run this periodically or after any route change. csrf_exempt is usually the bug

CSRF Protection in Real Applications

Online banking transfers view the HTML of any bank's transfer form and you will find a hidden input like <code>&lt;input type="hidden" name="csrf_token" value="..."&gt;</code>. Submit without it and the server returns 403. This is the pattern CSRF was originally invented to protect.

Django admin log into /admin/ and inspect any save form. Django injects a <code>csrfmiddlewaretoken</code> hidden field automatically via the <code>{% csrf_token %}</code> tag. Disable the middleware in settings and the entire admin stops accepting POSTs.

GitHub PR buttons click "Merge pull request" and the request includes an authenticity_token. GitHub's Rails backend checks it against the one it stored for your session. The token rotates per session, which is why an old HAR capture cannot be replayed.

Stripe Dashboard logout even the "log out" link in most SaaS dashboards is a POST with a CSRF token, not a GET. Otherwise an attacker could embed a logout link on another site and log users out by surprise, which is a CSRF attack even without stealing money.

Common CSRF Protection Mistakes to Avoid

Thinking HTTPS alone protects against CSRF

HTTPS encrypts the request in transit, but the attacker's page is not trying to read the traffic. It is just triggering a new, valid-looking request from the victim's browser. The connection is encrypted and the forgery still works. HTTPS stops a different attack (eavesdropping), not this one.

Adding @csrf_exempt to "just make it work"

A form breaks, the error mentions CSRF, and someone disables the check on that view to unblock the deploy. The form now ships to production with no protection. If the view changes state, it is a live CSRF hole. Fix the real cause (missing template tag, cookie domain, wrong method) instead of exempting the route.

Putting the token in the URL

Tokens belong in hidden form fields or request headers, not query strings. URLs leak into server logs, browser history, referrer headers, and analytics. A token that shows up in a log file is as good as a stolen password for the window it is valid.

Skipping CSRF for a "stateless" API that still uses cookies

If your API is authenticated by a session cookie (because your SPA talks to your same-origin backend), it is not stateless from the browser's point of view. The cookie auto-attaches, so CSRF applies. Either switch to bearer tokens in the Authorization header, or keep CSRF on and use the double-submit pattern.

Go Deeper on CSRF Protection

CSRF Protection Interview Questions →

4 common interview questions about csrf protection, with clear practical answers.

Related Building Blocks

Also known as: csrf, xsrf, cross site request forgery, csrf token, anti csrf token, samesite cookie, double submit cookie

COURSE

Ready to Build Real Products?

Learn to ship MicroSaaS apps with AI in the Solo Builder course.

Start Building →