flawopen.com/Cross-Site Scripting/JavaScript

Cross-Site Scripting in JavaScript

High CWE-79 Draft — pending review
Language: English Português (Brasil) Español Français Deutsch Русский 简体中文 日本語 हिन्दी 한국어 Bahasa Indonesia
ELI5

Imagine a guestbook where visitors write a public note. Stored XSS is like someone writing a note that isn't just text — it's a hidden trick that makes the guestbook page itself start doing things, like stealing the next visitor's login session, the moment anyone opens the page to read it.

Key terms on this page
output encoding
Converting characters that have special meaning in HTML (like < and >) into harmless equivalents before inserting untrusted text into a page, so the browser displays it as text instead of running it as markup or script.
DOM
The in-memory tree structure a browser builds from a page's HTML — where an element's innerHTML is set determines whether inserted content is rendered as inert text or executable markup.

What's happening

Cross-site scripting happens when untrusted input is rendered into a page without proper output encoding. In JavaScript, the most direct route is element.innerHTML — assigning a string to it parses that string as HTML, including any <script> tags or event-handler attributes it contains.

Real-world impact

In 2005, an 19-year-old user exploited a stored XSS flaw on MySpace to create the "Samy" worm — a script that added itself to every profile that viewed it, infecting over a million profiles within about 20 hours and forcing MySpace offline to contain it. It remains one of the most-cited demonstrations of how quickly stored XSS can self-propagate.

Source: widely documented in security industry retrospectives — see References below.

Vulnerable vs. fixed

VULNERABLE
// comment text parsed as HTML
function renderComment(el, comment) {
  el.innerHTML =
    `<p>${comment}</p>`;
}
FIXED
// comment text inserted as plain text
function renderComment(el, comment) {
  const p = document.createElement('p');
  p.textContent = comment;
  el.appendChild(p);
}

Why the fix works

textContent inserts a string as literal text — the browser never parses it as markup, so a <script> tag inside comment is displayed as visible characters, not executed. innerHTML can't offer this guarantee, because parsing the string as HTML is exactly what it's designed to do.

JavaScript-specific gotchas

React's name for its escape hatch says exactly what it does

JSX expressions like {comment} are escaped automatically. dangerouslySetInnerHTML exists specifically to opt out of that — the name is a deliberate warning, and it should be treated as one.

jQuery's .html() is innerHTML with a friendlier name

$(el).html(comment) has the exact same parsing behavior as el.innerHTML = comment — jQuery adds no protection here, despite feeling like a "safer" abstraction.

document.write() and URL-based sinks are easy to overlook

document.write(userInput) and setting location.href or an href/src attribute from unvalidated input (including a javascript: URI) are both real XSS sinks that don't involve innerHTML at all.

Common misconceptions

"React/Vue protects me automatically"

True for standard template/JSX interpolation — false for explicit raw-HTML escape hatches (dangerouslySetInnerHTML, Vue's v-html).

"This only matters for public comment sections"

Any value rendered into the DOM counts — a URL query parameter echoed into a search results header, a filename shown after upload, a value read from localStorage that another script wrote.

How to check if you're affected

grep -rn "\.innerHTML\s*=" --include="*.js" --include="*.ts" . grep -rn "dangerouslySetInnerHTML\|v-html" --include="*.jsx" --include="*.tsx" --include="*.vue" . grep -rn "document.write(" --include="*.js" .
eslint-plugin-react's no-danger rule and eslint-plugin-security's DOM-sink checks both catch these patterns automatically in CI.

Prevention checklist

FAQ

Is this the same bug as SQL injection?

Same underlying shape — untrusted data mixed into a command's structure without encoding — but the target and damage differ: SQL injection targets the database, XSS targets other users' browsers.

If I sanitize with a library like DOMPurify, is innerHTML safe again?

Yes, when the sanitizer runs on the untrusted string immediately before assignment and is kept up to date — the risk returns if sanitization is skipped for any code path or the library is misconfigured to allow script-bearing tags/attributes.

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)
Also see: SQL Injection Command InjectionCSRF Open Redirect