Skip to content

Cross-Site Scripting (XSS)

XSS = execution of attacker code in the victim’s browser, inside the app’s origin. The server isn’t broken; the browser’s trust in the code the app hands it is. Because that code runs in the victim’s origin, it inherits all of its client-side power: read and modify the DOM, use their cookies and tokens, fire authenticated requests and spoof the UI. A stored XSS in an admin panel is, in practice, platform compromise.

What the attacker gains depends on where the XSS fires and what protections exist:

  • No HttpOnly → direct session cookie theft (document.cookie).
  • With HttpOnly → can’t read the cookie, but acts on behalf of the victim (cookies ride along on the browser’s own requests).
  • Tokens in localStorage/sessionStorage → always reachable from JS.
  • Internal/admin panel (stored or blind) → user creation, config changes, pivot.

The browser interprets several languages (HTML, JS, CSS, URL). XSS is born when controllable data lands where the parser treats it as code, because the app didn’t encode it for that context or sanitize it. The Same-Origin Policy isolates origins, but the payload runs inside the vulnerable origin, so SOP works in the attacker’s favor.

In DOM-based XSS the key flow is source → sink:

  • Sources (controllable input): location (.href/.search/.hash/.pathname), document.referrer, document.cookie, window.name, postMessage, localStorage, API responses reflected into the DOM.
  • Sinks (execution): eval, Function, setTimeout/setInterval with a string, element.innerHTML/outerHTML, document.write/writeln, insertAdjacentHTML, element.setAttribute (of href/src/on*), location/location.href, iframe.srcdoc, script.src/text, and in jQuery $(), .html(), .append().

The valid payload depends entirely on where the data lands:

  1. Between tags <div>HERE… (between tags) → inject an executing element.
  2. Quoted attribute value="HERE" → close the quotes + handler, or autofocus onfocus=.
  3. Unquoted attribute value=HERE → a space is enough to add attributes.
  4. Inside JavaScript var x='HERE' → close the string/statement, or break a template literal `...${HERE}...`.
  5. In a URL href="HERE" → the javascript: scheme.
  6. In CSS style="HERE" → expression() (legacy), exfil via background:url().
  • Reflected: payload rides the request (query, header, body) and returns in the immediate response. Requires delivering a link/form to the victim.
  • Stored (persistent): the payload is saved (comment, profile, name, ticket, file metadata) and served to every visitor. Highest impact; in social apps it can self-propagate (worm, e.g. Samy).
  • DOM-based: injection happens client-side only; server HTML may be harmless. Server-side output encoding does not mitigate it.
  • Blind: fires where you can’t see it (ticket viewer, logs, backoffice). Detected and exploited out-of-band.
  • mXSS (mutation): the sanitizer emits “safe” HTML the browser re-parses when normalizing the DOM (HTML/SVG/MathML namespace confusion, noscript, template), resurrecting the vector. Breaks poorly designed sanitizers.
  • Self-XSS: requires the victim to paste the payload; low impact unless escalated with clickjacking or social engineering.
  • UXSS: a browser or extension bug breaking SOP; affects any site.
  • Inject a unique marker (pwn7h3) into every GET/POST parameter, fragment, header (Referer, User-Agent, X-Forwarded-*), cookie and JSON field. Locate all reflections and classify each context before trying a single payload.
  • Try context-breaking characters and check whether they return unencoded: < > " ' / = { }`.
  • DOM XSS: with DOM Invader (Burp) or by hand in DevTools, trace source→sink; look for innerHTML, eval, framework sinks, postMessage without origin validation.
  • Blind XSS: plant OOB-callback probes in name, address, user-agent, support/CRM fields; wait for backoffice execution.

�0�

  1. Start a listener on an IP the victim can reach: �1�
  2. Inject the vector into the reflected or stored point: �2�
  3. When the page renders (the victim, or an admin for stored/blind), you receive the cookie in your log.
  4. If it’s HttpOnly, act on their behalf from their browser (the cookie rides along): �3�
  5. For blind XSS, use an OOB domain and wait for async execution.
  • Session credential theft: document.cookie, localStorage tokens.
  • Authenticated actions via fetch/XHR (credentials:'include'): change email/password (→ ATO), create users, approve transactions.
  • Anti-CSRF token theft by reading the DOM/responses, to chain further.
  • Keylogging and form capture (addEventListener('input'...)).
  • In-page phishing (overlay a fake login on the legitimate domain).
  • Internal network scanning and client-side SSRF; in desktop apps (Electron) an XSS can escalate to RCE via nodeIntegration.
  • Persistence by registering a malicious service worker.
  • Encodings: HTML entities (&#x61;, &#97;), URL and double-URL, unicode (a), JS hex, nested. The browser decodes per context; the filter often doesn’t.
  • Tag/handler obfuscation: mixed case, uncommon tags (<svg>, <math>, <marquee>), alternative handlers (onpointerover, ontoggle, onanimationstart, onfocus+autofocus), separators (/, %0a, %0c), unquoted attributes.
  • Without blocked keywords: import(name) instead of eval, build strings with String.fromCharCode, atob, concatenation.
  • HTML parser quirks: malformed comments, unclosed <, foreign content (SVG/MathML) that changes parsing rules (basis of mXSS).

A poorly designed CSP doesn’t stop XSS:

  • unsafe-inline or a broad CDN whitelist → almost always bypassable.
  • JSONP endpoints on allowed domains → load your callback.
  • Script gadgets: allowed libraries with eval/templates (AngularJS ng-app, Vue template mode) execute expressions.
  • Unrestricted base-uri → hijack <script> relative paths.
  • Leaked/reused nonce or strict-dynamic loading a gadget.
  • Dangling markup / exfiltration: when you can’t execute, steal DOM data with a dangling tag (<img src='//oob.tld?), DNS-prefetch, or <link>.

Check the policy with CSP Evaluator and look for known gadgets.

  • File uploads: SVG with <script>, HTML served inline, reflected filenames, reflected EXIF metadata.
  • PDF generators / HTML→PDF converters: XSS → SSRF/local file read on the render server.
  • Markdown / WYSIWYG editors: raw HTML allowed, javascript: in links.
  • Headers and errors: reflected XSS in error pages or header-reflecting responses.
  • postMessage: receivers that innerHTML event.data without validating event.origin.

Burp Suite (+DOM Invader), dalfox, Gxss/kxss, arjun/paramspider (parameter discovery), XSS Hunter self-hosted / interactsh (blind/OOB), BeEF (post-exploitation demo), CSP Evaluator (policy audit).

Exfiltrate over a discreet own channel (image beacon, fetch keepalive, DNS), avoid noisy alerts, use neutral OOB domains, and keep scope and data to what the engagement allows.

  • CSP in report mode (Content-Security-Policy-Report-Only + report-to/ report-uri): violations expose unauthorized scripts and injections in production, including DOM XSS.
  • WAF/IDS: signatures (<script, onerror=, javascript:, srcdoc) plus anomaly detection (length, entropy, nested encodings) — signatures alone get bypassed.
  • SIEM: correlate input with HTML metacharacters later reflected in 200 responses; spikes of outbound requests to unknown domains (beacons) from user sessions; for internal blind, alert if the backoffice calls uncatalogued external domains.
  • Honeytokens: decoy cookies/fields that, if exfiltrated, raise an alert.

CSP reports, access logs with body/parameters, proxy/egress, RASP or response- inspecting WAF, and integrity checks of stored content.

  1. Context-aware output encoding — the root fix. Use the framework’s (React/Angular/Vue autoescape, safe t()) and OWASP’s (Java Encoder, etc.). Risk returns with dangerouslySetInnerHTML (React), v-html (Vue), bypassSecurityTrust* (Angular): if you must render user HTML, sanitize with DOMPurify with a restrictive config.
  2. Nonce/hash-based CSP with strict-dynamic, no unsafe-inline, with base-uri 'none' and object-src 'none'. Roll out in Report-Only first.
  3. Trusted Types (require-trusted-types-for 'script') to close DOM XSS sinks in Chromium browsers.
  4. Cookies HttpOnly + Secure + SameSite; don’t store session tokens in localStorage.
  5. Input validation via allow-list as an extra layer (never the only one).

Purge the stored payload, invalidate and rotate affected sessions/tokens, review which accounts may have been compromised (changed emails/passwords, created users), add a specific detection rule, and run a post-mortem on the injection point.

  • Samy (MySpace, 2005) — stored XSS that self-replicated by adding the attacker as a friend; ~1 million profiles in 20 hours. The most-cited XSS worm in history.
  • Twitter “onMouseOver” (2010) — stored XSS in the timeline: hovering a tweet ran JS and auto-retweeted it; spread in minutes.
  • TweetDeck (2014) — stored XSS in tweet rendering; a single payload tweet mass-auto-retweeted.
  • British Airways / Magecart (2018) — malicious client-side JS (skimmer) exfiltrated payment data from ~380,000 transactions. A showcase of the impact of running untrusted JavaScript in the victim’s origin.
  • Sanitizer bypasses (mXSS) — DOMPurify and others have shipped multiple mutation XSS advisories; keeping the sanitizer updated is part of the defense.

For product-specific CVEs, check the official feeds: NVD (https://nvd.nist.gov/vuln/search) and GitHub Security Advisories (https://github.com/advisories). Most real XSS is published as a CVE of the affected component.

  • Reflections of every parameter/header/cookie, with context identified.
  • Per-context testing: tag, attribute (quoted/unquoted), JS, URL, CSS.
  • DOM XSS: source→sink reviewed (incl. postMessage).
  • Blind: OOB probes in every field that ends up in an internal panel.
  • If CSP present: evaluated and gadgets/JSONP/base-uri tested.
  • Impact proven (cookie/authenticated action) and documented with a minimal PoC.