jQuery DOM-based cross-site scripting

DOM-based cross-site scripting with jQuery

Description

DOM XSS can occur when text or attribute values read from the DOM, such as textContent, innerText, value or attr, are subsequently interpreted as HTML. jQuery may parse a user-controlled string passed to $() as markup; .html() or .append() may also insert tags and event handlers. Reading previously escaped content as text and interpreting it again as HTML can undo the protection at the earlier output point.

Potential impact

  • Scripts may steal accessible cookies, tokens or browser-storage values and impersonate users.
  • Page content, form input or CSRF tokens may be read and sent externally.
  • An attacker may change the DOM or display phishing forms and pop-ups.
  • Scripts may call backend APIs or change account settings with the user's permissions.
  • Values retained in attributes, storage or URLs may be reinterpreted on later visits.

Remediation

  • Output DOM text and attribute values with textContent, innerText or jQuery's .text() rather than interpreting them as HTML.
  • For selectors, use $.find(str) instead of jQuery($str) to avoid HTML parsing, and restrict allowed selectors with an allow-list.
  • If markup is necessary, sanitize it with a maintained tool such as DOMPurify, allowing only required tags and attributes.
  • Avoid passing untrusted strings to .html(), .append(), .before() or .after(). Use text nodes or safely constructed DOM nodes where appropriate.
  • Validate or sanitize user-controlled query/hash values, data-* attributes and form values for their actual use.
  • Use CSP to restrict inline scripts as an additional defense.

Examples

Options A, B and C serve different purposes: plain text output, restricted HTML and selection of approved elements. Avoiding HTML parsing does not itself restrict which elements can be selected; keep the allow-list.

Before

javascript
// Reinterpret a DOM string as HTML before insertion (unsafe)
$(function () {
  // User-controlled data-content restored from a template, storage or URL
  const raw = $("#card").attr("data-content");
  // 1) Pass the raw string to .html(): event handlers or scripts may execute
  $("#detail").html(raw);

  // 2) jQuery($str) may parse the user-controlled string as HTML
  const $frag = $(raw);
  $("#detail").append($frag);
});

After

javascript
// Treat plain strings as text and selectors only as selectors
// Includes an optional sanitizer example
$(function () {
  const raw = $("#card").attr("data-content");

  // (A) Display as text
  $("#detail").text(raw); // No HTML reinterpretation

  // (B) Use a sanitizer if HTML is required
  // const safe = DOMPurify.sanitize(raw, {ALLOWED_TAGS: ['b','i'], ALLOWED_ATTR: []});
  // $('#detail').html(safe);

  // (C) Use $.find instead of $() for a user-controlled selector
  const sel = $("#btn").attr("data-target");
  // Example allow-list or pattern check
  const allowed = ["#info", ".section"];
  if (allowed.includes(sel)) {
    $.find(sel).forEach(function (el) {
      el.classList.add("active");
    });
  }
});

Explanation:

  • Before: A user-controlled attribute value is passed to .html() or $() and interpreted as markup. Injected event handlers may execute script.
  • After: .text() displays text without HTML interpretation. The optional markup path uses DOMPurify, while selectors are handled by $.find and restricted to an allow-list.

References