Skip to main content

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

  1. Log into DVWA.
  2. Set DVWA Security to Low.
  3. 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:

  1. Input is accepted into persistent storage without sufficient sanitization for the intended use (plain text vs rich HTML).
  2. 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

  1. Treat stored user text as data, not HTML—escape on read for the right context.
  2. If rich text is required, use a strict allowlist sanitizer (e.g. known-safe tags) and CSP.
  3. 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.cookie if HttpOnly were missing?

Evidence checklist

  • Screenshot of stored entry containing the payload.
  • Proof of execution (alert or DOM proof).
  • Short note: one sentence on why stored XSS is higher impact than reflected for multi-user pages.

Next steps