Stored XSS
Lab: DVWA
Scenario: Stored Cross-Site Scripting
Difficulty: Intermediate
Estimated Time: 30 minutes
Learning Objectives
By the end of this scenario, you will be able to:
- Contrast stored vs reflected XSS (who loads the payload, and when).
- Show that one stored payload can affect many users without clicking a crafted URL each time.
- Explain why output encoding at render time matters even if the database “stores HTML.”
Setup
Prerequisites
- DVWA at CSN Labs, security Low
- Browser with DevTools
Initial configuration
- Log into DVWA.
- Set DVWA Security to Low.
- Open Vulnerabilities → XSS (Stored).
Scenario story
A guestbook (or comment) feature saves user input and shows it to everyone who views the page. If the stored text includes HTML/script and the app renders it unsafely, every visitor executes the attacker’s script—wider blast radius than reflected XSS.
Step-by-step walkthrough
Step 1: Inspect the form
On XSS (Stored), note fields (commonly Name and Message). Submit benign text in both.
Expected: Your entry appears in the list below, persisting after refresh—proof of server-side storage.
Step 2: Inject a script in the message
In the Message field (or the field that renders rich content), submit:
<script>alert('stored')</script>
Use a short name if required.
Expected (Low): After save, the script executes when the page loads (you may see alert). In some builds, execution appears on first render or when the entry is listed.
Observe: In Elements, locate your <script> node in the DOM. Stored XSS survives across reloads until the entry is removed.
Step 3: Victim perspective
Open the same page in another browser profile (or incognito) if your lab allows—any user loading the guestbook should hit the same stored markup.
Learning point: The attacker does not need to phish a unique URL for every victim; they poison shared content.
Step 4: Compare to reflected
Ask: Where does the payload live? In stored XSS it lives in the backend store (database/file), not only in the query string.
Why this works
Stored XSS happens when:
- Input is accepted into persistent storage without sufficient sanitization for the intended use (plain text vs rich HTML).
- Output is rendered into HTML without encoding for the current context.
Filtering on input alone is fragile; safe systems encode on output or use a vetted HTML sanitizer only if rich text is required.
Defender notes
How to detect
- DB/content review for
<script,onerror=,javascript:in user fields. - Scanners and manual tests on every display path (list view, API JSON consumed by front-end).
- Alerting on sudden spikes in guestbook/comment posts from one account.
How to prevent
- Treat stored user text as data, not HTML—escape on read for the right context.
- If rich text is required, use a strict allowlist sanitizer (e.g. known-safe tags) and CSP.
- HttpOnly cookies reduce session theft impact of XSS but do not fix XSS itself.
Try these variations
Easy
- Store a benign
<b>bold</b>—does HTML render? That tells you if a markup policy exists.
Medium
- Try splitting payloads across requests or using case/encoding variants if filters exist at higher DVWA levels.
Hard
- Pair with session hijacking narrative: what could a script exfiltrate from
document.cookieif HttpOnly were missing?
Evidence checklist
- Screenshot of stored entry containing the payload.
- Proof of execution (
alertor DOM proof). - Short note: one sentence on why stored XSS is higher impact than reflected for multi-user pages.
Next steps
- Authentication bypass scenarios (brute force and session weaknesses).
- DVWA overview.