XSS Security Lab

Reflected XSS Demonstration

Test Your Knowledge

VULNERABLE - Reflected XSS
Medium-Hard (Filter Bypass)

Lab #6: Reflected XSS in Search Results

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.

Challenge: Get a JavaScript popup (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.
blocks: <script> tags blocks: onerror= blocks: literal "alert("
Popup executed — you found a filter bypass! Open "Show Solution" to see why it worked.
The filter checks for the literal string 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!
}

Why the Filter Failed

This lab has two independent bugs stacked together:

  • Blocklisting is incomplete by nature. The filter only checks 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)>
  • Blocking a function name doesn't block the capability. JavaScript can call 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).
  • Single-pass replace can be defeated by nesting. A regex that strips one occurrence of a pattern can be beaten by embedding the pattern inside itself, so that removing the "bad" substring reconstitutes it — the classic <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:

  • Never rely on a blocklist as your only defense — allowlists and context-aware output encoding are the actual fix; a blocklist is at best a secondary speed bump.
  • If rich formatting is genuinely required (bold matches, etc.), use a dedicated, actively-indextained sanitizer (e.g. DOMPurify) instead of hand-rolled regex.
  • Add a strict Content-Security-Policy (no unsafe-inline) as defense in depth — it would have blocked every payload above even if the filter had been bypassed.
  • Fuzz-test filters against known bypass lists (e.g. PortSwigger's XSS cheat sheet) before trusting them in production.