July 10, 2026
Reflected XSS into HTML context with most tags and attributes blocked
Hello hunters,

By zero_day
4 min read
Today, I solved another reflected XSS lab. And this one had a bouncer at the door — a WAF that blocked basically every tag and attribute I threw at it.
I walked in with <script>alert(1)</script> like I always do.
The WAF looked at me and said: "Tag is not allowed."
Rude. But fair.
The Lab
This one is PortSwigger's Reflected XSS into HTML context with most tags and attributes blocked.
The setup: a search feature, reflected right back into the page. Classic. Except this time, there's a WAF sitting in front of it, filtering out all the usual suspects — <script>, <img>, <svg>, onerror, onload, all of it.
The goal: pop print(). Somehow.
The Guessing Phase (a.k.a. hitting a wall repeatedly)
I tried the greatest hits first. <script>. <img onerror=...>. <svg onload=...>.
All rejected. Same error every time: "Tag is not allowed".
At some point you have to admit defeat and stop typing random tags into a text box like you're going to get lucky. So I opened Burp Intruder instead.
Press enter or click to view image in full size
(Yes, Community Edition. Yes, it's throttled. No, that doesn't matter for a payload list this size.)
The Fuzzing
Instead of guessing tags one at a time like an animal, I set up a Sniper attack with the tag name as the payload position:
GET /?search=<§tag§>hello HTTP/1.1GET /?search=<§tag§>hello HTTP/1.1Loaded a list of HTML tags from PortSwigger's own XSS cheat sheet. Hit start. Went to make tea.
Came back, sorted by status code and response length, and there it was — request #20, status 200, a completely different length from all the 400s around it.
body → 200 OKbody → 200 OK<body> got through. Everything else got the boot.
Now I needed an attribute. Same exact process, new fuzz position:
GET /?search=<body §attr§='alert(1)'>hello HTTP/2GET /?search=<body §attr§='alert(1)'>hello HTTP/2Loaded a list of event handlers this time. onclick, onload, onerror, all the classics — blocked. But a bit further down the list, one lit up green:
onresize → 200 OKonresize → 200 OK
The Realization (that made me feel very smart, then very dumb)
<body onresize='alert(1)'> — got reflected cleanly. I checked dev tools. Nothing fired.
I stared at the DOM inspector for a solid five minutes, absolutely convinced the WAF had somehow still eaten my payload, even though the raw response clearly showed it sitting there, untouched.
Then I resized my browser window. Out of pure frustration, not strategy.
alert(1) popped.
It was never broken. onresize just doesn't fire on page load — it fires when the body's dimensions actually change. And nothing in the DOM inspector tells you "hey, this payload works, it's just waiting for its trigger." You only find out by triggering it.
Lesson relearned the hard way: inspecting the DOM does not fire events. Only real interaction does. I'd been treating "nothing visibly happened" as "this failed," when actually it just meant "this is patiently waiting."
The Actual Problem
Great, so I have a working payload. One small issue: my victim is not going to sit there resizing their own browser window for me. That would be a very considerate victim, and also not how phishing works.
I needed to trigger the resize for them, without them lifting a finger.
Enter: the iframe trick.
The Exploit
Load the vulnerable page inside an iframe on the exploit server, then immediately shrink the iframe the moment it finishes loading:
<iframe
src="https://YOUR-LAB-ID.web-security-academy.net/?search=%3Cbody%20onresize%3Dprint()%3E"
onload="this.style.width='100px'">
</iframe><iframe
src="https://YOUR-LAB-ID.web-security-academy.net/?search=%3Cbody%20onresize%3Dprint()%3E"
onload="this.style.width='100px'">
</iframe>Here's what's actually happening:
- The iframe's
srcloads the vulnerable search page, with my payload<body onresize=print()>sitting in the results. onloadfires the second the iframe content finishes loading.- Inside
onload, I shrink the iframe's width to100px. - Shrinking the iframe forces the
<body>element inside it to resize. onresizefires.print()runs. No victim interaction required.
Clicked View exploit first, just to double check. print() fired on its own, no window-dragging required.
Clicked Deliver to victim.
Lab solved.
What I Learned
A few things stuck with me from this one:
Fuzz, don't guess. Typing tags into a search box one at a time is how you burn an hour doing what Intruder does in five minutes while you make tea.
A blocked combo isn't always what you think it is. My first onresize attempt used single quotes around the JS — onresize='print(1)' — and got mangled. Not because body or onresize were blocked (I'd already confirmed both worked individually), but because of the quote characters themselves. Test your punctuation as its own variable, not just your tags and attributes.
"Nothing happened" isn't the same as "it failed." Event handlers sit quietly until their event fires. The DOM inspector will never tell you a payload works — it can only show you the payload is there. You have to trigger the event yourself to know.
When you can't rely on the victim to act, force the trigger. onresize needing an actual resize sounds like a dead end until you remember you control an iframe, and iframes can be resized by JavaScript the instant they load. No click, no scroll, nothing required from the person on the other end.
Closing
This lab took longer than it should have, purely because I distrusted my own working payload for five straight minutes. Turns out the WAF wasn't the hard part. My own assumptions were.
More labs coming. Till then — resize responsibly.