Step 138. XSS Basics — Reflected & Stored, the Traitor Inside the Browser
Level 2 — Introduction to Security and Attack Skill Basics | Difficulty ★★★★☆ | Estimated time: 3 hours
Prerequisites: Step 134’s cookies and HttpOnly, and Step 135–137’s injection mindset (input becomes syntax).
- What you need: Python 3 + Flask, curl or Python requests, a browser (for confirming pop-ups, optional).
- ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
- Caution: this environment has no browser automation, so pop-up scenes appear as screen examples. Instead, the essence of XSS — whether my script is alive in the response HTML — is fully measured by inspecting response bodies directly. When you confirm in a browser, the same "check life or death with View Source" procedure you learn today applies.
The injections so far (SQL, command) attacked the server. XSS (Cross-Site Scripting) is a different breed — it passes through the server to attack another user’s browser. When a <script> I planted executes in the victim’s browser, I can do anything with that page’s authority. The representative goal is Step 134’s target: cookie theft. Today you’ll reproduce both kinds of XSS — one-shot Reflected and persistent Stored — and understand structurally why Stored is far more dangerous.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain how XSS differs from SQL injection (the attack target is the browser)
- Distinguish Reflected XSS from Stored XSS in terms of damage scope
- Follow the procedure of inspecting the response body to check whether a payload is alive
- Explain the principle of variant payloads (
<img onerror>,<svg onload>) when<script>is blocked - Demonstrate by experiment why output escaping (
htmlspecialchars,html.escape) is the fundamental defense
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 + Flask (vulnerable board), curl/requests (response inspection), browser (pop-up confirmation), DVWA (optional, output examples) |
| Today’s payloads/code | <script>alert(1)</script>, <img src=x onerror=alert(1)>, html.escape() |
| Concepts needed | Output context (where in the HTML input lands), Reflected vs Stored, HTML escaping |
| Today’s artifact | lab138.py (vulnerable board) + a Reflected/Stored reproduction record |
2-1. The Root of XSS — That Familiar Root
Bring back the sentence you memorized in Step 135: injection is born when input becomes syntax instead of data. XSS is the same. Only the owner of the syntax is HTML, not SQL, and the execution site is the browser, not the DB.
If a server stores and prints the user input <script>alert(1)</script> as a "string," the screen just shows those characters. But if it splices it straight into the HTML document, the browser interprets it as a tag and executes the script. Mixing code and data in one bowl — the common ancestor of the injection family.
2-2. Reflected vs Stored — Single-Use vs Persistent Round
- Reflected XSS: input is immediately reflected in the request’s response. The case where a value entered in a search box reappears on the results page. To attack, you must make the victim click a crafted link, and execution happens once, for that person.
- Stored XSS: input is stored on the server and executed for every visitor who opens the page afterward. Board posts, comments, profile names are the stage. No need to spread links — victims come on their own.
That’s why Stored is far more dangerous — the victim count becomes not "people who clicked the link" but "everyone who viewed the post." Today you’ll reproduce both and feel the difference.
2-3. alert(1) Is Only Proof
The beginner-lab pop-up alert(1) is not the goal — it’s a certificate. Proof of the fact that "a script executes here." Real payloads start from alert(document.cookie) and continue into code that sends the cookie to the attacker’s server. And the HttpOnly you learned in Step 134 becomes the shield exactly here — it stops JS from reading the cookie. The chapters interlock like this.
2-4. Output Context — Where Did My Input Land?
Even the same input needs a different payload depending on where it lands.
- HTML body (text position):
<script>...</script>works as-is - Inside a tag attribute (
<input value="here">): you must close the attribute with"first - Inside a JavaScript string: quote escaping comes first
Today we focus on the first (body context) and plant only one habit: "look at the location first."
3. Follow Along
3-1. Launching the Vulnerable Board
lab138.py (educational vulnerable code — never deploy it anywhere):
from flask import Flask, request
app = Flask(__name__)
GUESTBOOK = [] # guestbook for the Stored experiment (in-memory)
PAGE = """<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Vulnerable Board</title></head>
<body>{body}</body></html>"""
@app.route("/hello")
def hello():
"""Reflected: reflect input straight into the response."""
name = request.args.get("name", "")
return PAGE.format(body=f"<h1>Hello, {name}!</h1>")
@app.route("/guestbook", methods=["GET", "POST"])
def guestbook():
"""Stored: store input and show it to every later visitor."""
if request.method == "POST":
GUESTBOOK.append(request.form.get("msg", ""))
return "Your post has been registered."
items = "".join(f"<li>{m}</li>" for m in GUESTBOOK)
return PAGE.format(body=f"<h1>Guestbook</h1><ul>{items}</ul>")
if __name__ == "__main__":
app.run(port=5138)
python lab138.py
The vulnerability is one line — input is spliced into the HTML as {body} with no processing at all.
3-2. Reflected — Normal and Injected
Normal input (measured 2026-09-09, name=alice):
<body><h1>Hello, alice!</h1></body>
Attack input (name=<script>alert(1)</script>) (measured 2026-09-09, response body):
<body><h1>Hello, <script>alert(1)</script>!</h1></body>
How to read it: my <script> is alive as a tag inside the response HTML. A browser receiving this response executes that part as a script, not text — open this URL in a browser and a 1 pop-up appears (the pop-up scene is a screen example). curl/requests won’t show a pop-up, but inspecting the body is sufficient to judge execution — if the tag is alive, the browser will definitely run it.
Why: this is Reflected’s structure — the attack code rides in the request (URL) and comes back reflected in the response. That’s why the victim needs a "click this link." A link whose URL is ...?name=<script>....
3-3. Stored — Plant It and Wait
Step 1 — Register (measured 2026-09-09):
curl -X POST "http://127.0.0.1:5138/guestbook" --data-urlencode "msg=<script>alert(document.cookie)</script>"
→ Your post has been registered.
Step 2 — Another visitor’s view (measured 2026-09-09, GET as a fresh request):
curl "http://127.0.0.1:5138/guestbook"
<body><h1>Guestbook</h1><ul><li><script>alert(document.cookie)</script></li></ul></body>
How to read it: the registration request’s response ("Your post has been registered") contained no script. Yet it came back alive in a later view request. After registering from your browser, open the guestbook in an incognito window or a different browser — the pop-up appears (screen example). The attacker has already left, but the attack keeps executing.
Why: this is Stored’s persistence. The server stored malicious input in the DB (here, an in-memory list) and ships it in every subsequent view response. If Reflected is a "phishing link," Stored is a "poisoned post." The damage spreads to every viewer.
3-4. When There’s a Filter — Variant Payloads
Say you’ve met a clumsy filter that only deletes <script>, like DVWA’s Medium difficulty (screen/output example). The bypass mindset splits in two.
- Case/nesting: if the filter looks for exactly
<script>,<Script>may pass (HTML tags are case-insensitive). There’s also nesting, exploiting the deletion’s trace, like<scr<script>ipt>. - Different tags: scripts don’t execute only through
<script>.
<img src=x onerror=alert(1)> ← nonexistent image, load fails → onerror executes
<svg onload=alert(1)> ← SVG finishes loading → onload executes
The common point is the event handler attribute (onerror, onload) — HTML has several syntaxes that execute scripts, so a filter blocking only <script> leaks by constitution. Note that DVWA at Medium and above blocks these too — comparing per difficulty what gets blocked is the fun of DVWA.
3-5. The Context-Checking Habit — View Source First
When alert doesn’t pop, the inspection order is fixed. First look at how your input landed in View Source (the response body).
- Landed as-is → an execution-condition problem (incomplete tag, browser blocking)
- Changed to
<script>→ the server escaped it — this input path is done - Only
<script>evaporated → a filter — try the 3-4 variants
What you did with curl in 3-2 is exactly this procedure. You see precisely the same information as the browser’s "View Source (Ctrl+U)."
3-6. The Defense Version — The Power of Escaping
Add a defense route to lab138.py and restart.
import html
@app.route("/hello_safe")
def hello_safe():
"""Defense: HTML-escape on output (equivalent to DVWA Impossible's htmlspecialchars)."""
name = request.args.get("name", "")
return PAGE.format(body=f"<h1>Hello, {html.escape(name)}!</h1>")
Output (measured 2026-09-09, same attack input):
<body><h1>Hello, <script>alert(1)</script>!</h1></body>
How to read it: < became < and > became >. The browser only draws < as the character "<" — it doesn’t interpret it as a tag. The input still shows on screen, but it can never be promoted to syntax. What htmlspecialchars() in DVWA’s Impossible code does in PHP is exactly this (output example).
Why: like SQLi’s parameter binding, XSS’s output escaping is a technique for "caging input as data." The defense of the injection family converges into one — never mix code and data.
4. Missions & Exercises
Mission — A Reproduction Report for Both Kinds of XSS
- Complete
lab138.pyand capture the requests and response bodies of Reflected (3-2) and Stored (3-3) — mark the part where the script is alive as a tag. - In the Stored experiment, arrange the captures so the contrast is visible: "not in the registration response, but present in the view response."
- Send the same payload to the defense route (3-6) and capture the escaped body.
- If you have a browser, add the actual pop-up scene; if you have DVWA, add the alert-success screens of the Reflected/Stored menus (both optional).
- In your wiki,
xss-basics.md— organize a Reflected/Stored comparison table (execution timing, damage scope, propagation path) and 3 lines on "why escaping blocks it."
Exercises
Exercise 1. Explain the claim that XSS grew from the same root as SQL injection (mixing code and data), contrasting each one’s "syntax" and "execution site."
Exercise 2. Explain why Stored XSS is more dangerous than Reflected, from the perspectives of damage scope and propagation path.
Exercise 3. Explain with the "event handler" concept why <img src=x onerror=alert(1)> can pass when <script> is deleted by a filter.
Exercise 4. Explain from the browser’s interpretation perspective why a <script> passed through html.escape() doesn’t execute. And write one line each on how what Step 134’s HttpOnly blocks differs from what output escaping blocks.
5. Model Answers & Completion Criteria
Mission Model Answer
How to verify: ① does the Reflected capture’s body contain <script>alert(1)</script> in tag form (per the 2026-09-09 measurement)? ② do the Stored captures contrast the POST response with the GET response — registration showed only "Your post has been registered," while the view response has the script inside <li>? ③ does the defense route’s body start with <script>? ④ does the summary table carry the contrast "Reflected = the one person who clicked the link / Stored = every viewer"?
Exercise Answers
Answer 1. In SQLi, input becomes SQL syntax and executes on the DB server; in XSS, input becomes HTML/JavaScript syntax and executes in the victim’s browser. Both share the root of "mixing user input into the same bowl as code for output/execution," and the defense shares the same philosophy too (caging input as data — binding / escaping).
Answer 2. Reflected requires getting the victim to click a crafted link, and execution happens that one time — propagation needs social engineering. Stored is saved on the server and executes automatically in the browser of everyone who views it afterward — the damage grows even after the attacker walks away. In the 3-3 measurement, the script being alive in a separate request after registration is the evidence.
Answer 3. Because the <img> tag’s onerror attribute is legitimate HTML syntax holding JavaScript that runs when image loading fails. Give a nonexistent address like src=x and loading necessarily fails, so alert(1) executes. If the filter looks only for the string <script>, this tag passes unharmed — the structural fact that HTML has multiple script-execution channels is the basis of filter bypass.
Answer 4. After escaping, < becomes the character reference <, so the browser’s HTML parser doesn’t recognize it as the start of a tag and only draws the character "<" on screen. If it isn’t parsed as a tag, there’s no script execution. The difference from HttpOnly: output escaping blocks the planting of the script itself, while HttpOnly blocks cookie theft even if a script gets planted — they’re defenses on different layers.
Completion Criteria Checklist
- [ ] I can explain that XSS’s execution site is the victim’s browser
- [ ] I can state the difference between Reflected and Stored by execution timing and damage scope
- [ ] I can follow the procedure of judging a payload’s life or death by inspecting the response body
- [ ] I reproduced Reflected injection and Stored save-and-view on a local server
- [ ] I can explain the principle of the
<img onerror>and<svg onload>variants (event handlers) - [ ] I confirmed by experiment what
html.escape/htmlspecialcharsdo - [ ] Mission: I wrote the reproduction report for both kinds
6. Common Pitfalls & Fixes
Wall 1. alert won’t pop — where do I look first?
Symptom: you entered the payload but the browser stays quiet.
The cause is one of three: ① the server escaped it, ② a filter deleted it, or ③ it landed somewhere it can’t execute (inside an attribute, etc.).
Fix: the order is fixed — first confirm your input’s final form in View Source (or the curl body). Apply 3-5’s three-way classification. Debugging by watching only whether the pop-up appears loses the trail. The body is the answer.
Wall 2. With curl, < shows up as %3C
Symptom: the value the server received is still encoded.
Cause: special characters in a URL need encoding, and if you attach them by hand without --data-urlencode, the server’s interpretation goes sideways. Conversely, servers sometimes print encoded values without decoding.
Fix: curl -G URL --data-urlencode "name=<script>alert(1)</script>", or Python requests.get(url, params={...}). Leave encoding to the tools.
Wall 3. It’s Stored, but refreshing right after registering shows nothing
Symptom: you wrote a post but it’s not in the guestbook.
Cause: restarting the server resets the in-memory store (the GUESTBOOK list). Or the POST went as a GET by mistake.
Fix: perform register (POST) → view (GET) consecutively within the same server run. That’s both the limit of an in-memory guestbook and the reason real services store in a DB.
Wall 4. I changed it to <Script> and it still doesn’t work
Symptom: the case variant doesn’t pass.
Cause: that server uses a case-insensitive filter, or it’s outright escaping. Variant payloads pass only against "clumsy string-replacement filters."
Fix: first look in View Source whether <Script> survived as-is. If it’s alive but didn’t execute, it’s another problem; if it became <, it’s escaping — close this path and find another input point (another parameter, another page).
Wall 5. The browser blocked the pop-up (XSS Auditor family)
Symptom (screen example): old Chrome showed ERR_BLOCKED_BY_XSS_AUDITOR.
Cause: past browsers had a Reflected XSS detector — a mechanism that blocked when a script in the request was reflected unchanged in the response. Modern Chrome removed the feature (bypasses were too easy and false positives too many).
Fix/lesson: don’t rely on browser defenses. And this chapter’s body-inspection procedure (3-5) holds whether the browser blocks or not — the very fact that the script is alive in the server response is evidence of the vulnerability.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| XSS | Injection where an input script executes in the victim’s browser |
| Reflected XSS | Rides the request, reflects in the response — once, for the one person who clicked the link |
| Stored XSS | Stored on the server, executes repeatedly for every viewer — far more dangerous |
| Output context | The HTML position where input lands — each position needs a different payload |
| Event handler | onerror/onload etc. — syntax that executes scripts without <script> |
| HTML escaping | < → < — the fundamental defense that blocks tag interpretation |
Today’s Commands & Payloads
| Command/payload | What it does |
|---|---|
<script>alert(1)</script> |
The basic proof-of-execution payload |
<script>alert(document.cookie)</script> |
Proof of cookie access (Stored practice) |
<img src=x onerror=alert(1)> |
Event-handler bypass |
curl -G URL --data-urlencode "name=..." |
Reflected test (check the response body) |
curl -X POST URL --data-urlencode "msg=..." |
Planting Stored |
html.escape(input) / htmlspecialchars(input) |
Output-escaping defense |
An Instinct More Important Than Commands
Half of studying XSS is attack, and half is the verification procedure — the habit of looking not at "did the pop-up appear" but at "is my tag alive in the response body." And now that you’ve seen Stored’s persistence firsthand, board posts, comments, and profile fields will look different. SQLi’s binding, XSS’s escaping — the defense of injection is ultimately one sentence: never mix code and data. That sentence runs through every chapter this week.
Once every box is checked, Step 138 is complete. Click the checkbox in the sidebar to save your progress.