In our previous posts, we discussed Stored XSS and Reflected XSS attacks. In this post, we’ll explore DOM-based XSS, a type of XSS vulnerability that occurs entirely in the browser without server-side involvement.

What is DOM-based XSS? Link to heading

DOM-based XSS occurs when client-side JavaScript code writes user-controlled data to the Document Object Model (DOM) in an unsafe way. Unlike Stored and Reflected XSS, the vulnerability exists entirely in the client-side code and doesn’t involve the server.

  1. An attacker crafts a malicious URL or input
  2. The victim’s browser processes the input
  3. JavaScript code unsafely writes the input to the DOM
  4. The browser executes the malicious script

Practical Example Link to heading

Let’s look at a vulnerable search application that demonstrates two common DOM-based XSS vulnerabilities

Project structure Link to heading

dom-based-xss-demo/
├── app/
│   ├── app.py
│   ├── Dockerfile
│   └── templates/
│       └── index.html
└── docker-compose.yml
# app/app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0') 
# app/Dockerfile
FROM python:3.9-slim

WORKDIR /app

RUN pip install --no-cache-dir \
    Flask==2.0.1 \
    Werkzeug==2.2.2

COPY app.py .
COPY templates ./templates

EXPOSE 5000

CMD ["python", "app.py"]
<!-- app/templates/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>DOM XSS Demo</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        .search-form {
            margin-bottom: 30px;
        }
        .search-form input {
            width: 100%;
            padding: 10px;
            margin-bottom: 10px;
        }
        .results {
            margin-top: 20px;
        }
        .no-results {
            color: #666;
            font-style: italic;
        }
    </style>
</head>
<body>
    <h1>DOM XSS Demo</h1>
    
    <div class="search-form">
        <form id="searchForm">
            <input type="text" id="search" placeholder="Search for something...">
            <button type="button" id="searchButton">Search</button>
        </form>
    </div>

    <div class="results" id="results">
        <!-- Results will be inserted here -->
    </div>

    <script>
        // Define the search function
        const search = function() {
            const query = document.getElementById('search').value;
            const resultsDiv = document.getElementById('results');
            
            // Vulnerable code: directly using user input in innerHTML
            resultsDiv.innerHTML = query;
        };

        // Add event listeners
        document.getElementById('searchButton').addEventListener('click', search);
        document.getElementById('searchForm').addEventListener('submit', function(e) {
            e.preventDefault();
            search();
        });

        // Process hash on page load
        if (location.hash) {
            document.getElementById('results').innerHTML = decodeURIComponent(location.hash.slice(1));
        }

        // Process hash changes
        window.onhashchange = function() {
            document.getElementById('results').innerHTML = decodeURIComponent(location.hash.slice(1));
        };
    </script>
</body>
</html> 
# docker-compose.yml
services:
  app:
    build: ./app
    ports:
      - "5000:5000"
    volumes:
      - ./app:/app

The application contains two DOM-based XSS vulnerabilities:

  1. Search Box Attack: User input is directly written to the DOM using innerHTML
  2. URL Hash Attack: URL fragment is decoded and written to the DOM

Run the project Link to heading

Start the container:

docker-compose up --build -d

Open http://localhost:5000 in your browser. You should see the search form.

Search Form

Attack Scenarios Link to heading

  1. Search Box Attack:

    • Enter this in the search box:
    <img src=x onerror=alert('XSS')>
    
    • Click the Search button or press Enter
    • You should see an alert popup with the message “XSS”

    Search Box Attack

  2. URL Hash Attack:

    • Visit this URL:
    http://localhost:5000/#<img src=x onerror=alert('XSS')>
    
    • The alert will execute when the page loads

    URL Hash Attack

Clean up Link to heading

Stop the container:

docker-compose down

Why This is Vulnerable Link to heading

The application is vulnerable because:

  1. Direct DOM Manipulation: User input is directly written to the DOM using innerHTML without any sanitization
  2. URL Fragment Processing: The URL hash is decoded and written to the DOM without validation
  3. No Input Validation: There’s no checking of the input content before it’s rendered

How to Prevent DOM-based XSS Link to heading

1. Avoid Unsafe DOM Manipulation Link to heading

Never use innerHTML with user input:

// Instead of:
resultsDiv.innerHTML = query;

// Use:
resultsDiv.textContent = query;

2. Use Safe DOM APIs Link to heading

Use safe DOM manipulation methods:

const h2 = document.createElement('h2');
h2.textContent = `Search Results for: ${query}`;
resultsDiv.appendChild(h2);

3. Sanitize User Input Link to heading

Use a library like DOMPurify to sanitize HTML:

import DOMPurify from 'dompurify';

const sanitizedQuery = DOMPurify.sanitize(query);
resultsDiv.innerHTML = sanitizedQuery;

4. Content Security Policy (CSP) Link to heading

Implement CSP headers to restrict script execution:

@app.after_request
def add_security_headers(response):
    response.headers['Content-Security-Policy'] = "default-src 'self'"
    return response