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,innerTextor jQuery's.text()rather than interpreting them as HTML. - For selectors, use
$.find(str)instead ofjQuery($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$.findand restricted to an allow-list.