Client-side DOM-based XSS

Client-side DOM-based XSS

Description

DOM-based XSS occurs when browser JavaScript inserts user-controlled values into HTML-interpreting APIs such as innerHTML, outerHTML, insertAdjacentHTML or document.write without appropriate handling. URL parameters, fragments and element values can carry malicious markup or event handlers. For example, a decoded ?q=<img src=x onerror=alert(1)> value inserted into innerHTML may execute JavaScript. A value previously handled as text can become dangerous if it is later reinterpreted as HTML.

Potential impact

  • Arbitrary JavaScript may run in a user's browser.
  • Scripts may steal readable cookies, tokens or localStorage data and compromise an account.
  • DOM manipulation may alter pages, intercept form input or expose internal information.
  • Deceptive forms or messages may support phishing.

Remediation

  • Use textContent or innerText for text; do not pass untrusted values directly to HTML-interpreting APIs.
  • If markup is required, restrict it with a trusted sanitizer such as DOMPurify or sanitize-html.
  • Apply handling appropriate to HTML text, attributes, URLs or JavaScript; these contexts are not interchangeable.
  • Parse URLs with URLSearchParams or new URL() and validate allowed values as needed.
  • Use CSP and, where supported, Trusted Types as additional browser defenses.

Examples

Before

javascript
// DOM output before input handling
(function () {
  // Read the URL fragment for DOM insertion; check the actual interpreted value
  const raw = location.hash.slice(1); // For example: #<img src=x onerror=alert(1)>
  const box = document.getElementById('msg');
  // BAD: Insert user input directly into an HTML context
  box.innerHTML = '<p>결과: ' + raw + '</p>';

  // Another risk: write user input with document.write
  const q = new URL(location.href).searchParams.get('q') || '';
  document.write('<div>검색어: ' + q + '</div>');
})();

After

javascript
(function () {
  const params = new URL(location.href).searchParams;
  const q = params.get('q') || '';
  const box = document.getElementById('msg');

  // 1) Display text without interpreting HTML
  box.textContent = `검색어: ${q}`;

  // 2) Sanitize markup when HTML is necessary
  // Assume DOMPurify has been loaded globally
  const userHtml = params.get('html') || '';
  const safeHtml = DOMPurify.sanitize(userHtml, {ALLOWED_TAGS: ['b','i','u']});
  document.getElementById('preview').innerHTML = safeHtml;

  // 3) Construct values appropriately for URL and other attribute contexts
  const link = document.getElementById('link');
  const dest = params.get('dest') || '/';
  // Example allow-list check
  const allowed = ['/', '/help', '/profile'];
  link.setAttribute('href', allowed.includes(dest) ? dest : '/');
})();

Explanation:

  • Before: User-controlled values reach HTML-interpreting APIs. URLSearchParams decodes query values, while reading location.hash does not percent-decode the fragment. Encoded tags in a fragment alone therefore do not establish execution; inspect how the actual HTML string is formed.
  • After: textContent displays text, DOMPurify restricts deliberately allowed markup, and the URL attribute selects only listed paths. The example assumes DOMPurify and the referenced page elements are already available.

References