Reflected XSS Demonstration
The obvious payload is blocked. Can you find what the filter missed?
Search boxes almost always echo the search term back — "Showing results for laptop". Real applications rarely ship with zero defenses though; more often a developer bolts on a quick filter after noticing the obvious attack, then assumes the problem is solved.
This lab simulates exactly that: a naive, WAF-style filter blocks the textbook payload. Your job is to find an input the filter doesn't account for. Real attackers do this constantly — regex-based filters almost always have gaps.
alert,
confirm, or prompt) to fire through the search box.
The filter below blocks the first thing you'll try — read the filter rules,
then think about what they don't cover.
onerror= and the literal
word alert( — but HTML has dozens of event-handler attributes, and
JavaScript has more than one way to pop a dialog. Also: the filter runs once,
not recursively — what happens if your blocked substring is hiding
inside a larger string that only becomes the blocked string
after part of it gets removed?
// VULNERABLE CODE (with naive filter)
function sanitize(input) {
// "Should be enough" - blocks the textbook payload only
let clean = input.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '');
clean = clean.replace(/onerror\s*=/i, ''); // only checks THIS one attribute
clean = clean.replace(/alert\s*\(/i, ''); // only checks THIS one function name
return clean;
}
function doSearch() {
const query = sanitize(document.getElementById('searchInput').value);
document.getElementById('searchResult').innerHTML =
'Showing results for: ' + query; // still innerHTML!
}
This lab has two independent bugs stacked together:
onerror=, but HTML has 70+ event-handler attributes:
onload, onfocus (combine with autofocus
on an <input>/<select>), onpointerover,
onwheel, onanimationstart (with CSS), etc. Example bypass:<svg onload=confirm(1)> or
<input autofocus onfocus=confirm(1)>
window.alert without ever typing the
literal string alert( — for example
confirm(1), prompt(1), or bracket-notation tricks
like top['al'+'ert'](1) and
window['ale'+'rt'](1).
<scr<script>ipt> trick for tag
filters.
The real fix isn't a smarter blocklist — it's not treating the sink as HTML at all:
// FIXED CODE - no filter needed, because there's no HTML parsing
function doSearch() {
const query = document.getElementById('searchInput').value;
// safe: browser renders this as literal text, never parsed as markup,
// regardless of what characters or attribute names it contains
document.getElementById('searchResult').textContent =
'Showing results for: ' + query;
}
Additional hardening for real-world search features:
Content-Security-Policy (no
unsafe-inline) as defense in depth — it would have
blocked every payload above even if the filter had been bypassed.