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
textContentorinnerTextfor 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
URLSearchParamsornew 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.
URLSearchParamsdecodes query values, while readinglocation.hashdoes 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:
textContentdisplays 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.