Frontend Labs
⋯

Day 70 — XSS Detection

Learning Objectives

  • Detect reflected XSS, script injection, and event handler injection
  • Normalize input before matching: HTML-unescape, URL-decode, unicode
  • Build the PII scrubber integration (Day 79)

Key Concepts

XSS Attack Vectors:
  <script>alert(1)</script>          — direct script injection
  <img src=x onerror=alert(1)>      — event handler injection
  javascript:alert(1)               — JavaScript URI scheme
  <svg onload=alert(1)>             — SVG attack
  <a href="javascript:void(0)">     — inline JS in href
  "><script>alert(1)</script>       — breaking out of attribute

Common Bypasses:
  <ScRiPt>alert(1)</sCrIpT>         — mixed case → normalize to lowercase
  %3Cscript%3E                      — URL encoding → URL decode first
  &lt;script&gt;                    — HTML entity encoding → HTML unescape first
  <scr\x00ipt>                      — null byte injection → remove null bytes

Exercise: XSS Detection Rule

package waf

import (
    "html"
    "net/http"
    "net/url"
    "regexp"
    "strings"
)

var (
    // Script tags in all forms
    xssScriptRe = regexp.MustCompile(`(?i)<\s*script[\s>]`)

    // JavaScript URI scheme
    xssJSURIRe = regexp.MustCompile(`(?i)javascript\s*:`)

    // Inline event handlers: onerror=, onload=, onclick=, etc.
    xssEventHandlerRe = regexp.MustCompile(`(?i)\bon[a-z]{2,20}\s*=`)

    // Dangerous tags
    xssDangerousTagsRe = regexp.MustCompile(`(?i)<\s*(iframe|frame|object|embed|applet|meta|link|base|form|input|button|select|textarea)\b`)

    // Data URI with script
    xssDataURIRe = regexp.MustCompile(`(?i)data:\s*text/html`)
)

type XSSRule struct{}

func (r *XSSRule) ID() string       { return "XSS-001" }
func (r *XSSRule) Category() string { return "XSS" }

func (r *XSSRule) Match(req *http.Request, body []byte) (bool, int, string) {
    inputs := extractInputs(req, body)
    for _, input := range inputs {
        normalized := normalizeXSS(input)

        if xssScriptRe.MatchString(normalized) {
            return true, 8, "XSS: script tag"
        }
        if xssJSURIRe.MatchString(normalized) {
            return true, 7, "XSS: javascript: URI"
        }
        if xssEventHandlerRe.MatchString(normalized) {
            return true, 6, "XSS: event handler injection"
        }
        if xssDangerousTagsRe.MatchString(normalized) {
            return true, 4, "XSS: dangerous HTML tag"
        }
        if xssDataURIRe.MatchString(normalized) {
            return true, 7, "XSS: data: URI with HTML"
        }
    }
    return false, 0, ""
}

// normalizeXSS applies multi-layer decoding before matching
// Order matters: URL decode → HTML unescape → lowercase → strip null bytes
func normalizeXSS(input string) string {
    // URL decode (handles %3C → <)
    decoded, err := url.QueryUnescape(input)
    if err != nil {
        decoded = input
    }

    // HTML unescape (handles &lt; → <)
    decoded = html.UnescapeString(decoded)

    // Double URL decode (handles %253C → %3C → <)
    if again, err := url.QueryUnescape(decoded); err == nil {
        decoded = again
    }

    // Remove null bytes (used to break pattern matching)
    decoded = strings.ReplaceAll(decoded, "\x00", "")

    // Lowercase for case-insensitive matching
    return strings.ToLower(decoded)
}

// Test payloads — all should trigger XSS detection:
var xssTestPayloads = []string{
    "<script>alert(1)</script>",
    "<ScRiPt>alert(1)</ScRiPt>",
    "%3Cscript%3Ealert(1)%3C/script%3E",
    "&lt;script&gt;alert(1)&lt;/script&gt;",
    "<img src=x onerror=alert(1)>",
    "javascript:alert(1)",
    "<svg onload=alert(1)>",
    "<iframe src=javascript:alert(1)>",
}

// Legitimate inputs that must NOT trigger:
var xssLegitInputs = []string{
    "Hello <World>",                           // angle bracket in text (no tag)
    "2 < 3 is true",                           // math comparison
    "onclick button behavior documentation",   // 'onclick' as a word
    "<b>bold</b> <i>italic</i>",               // common HTML in CMS
    "script writing techniques",               // 'script' as a word
}

Real-World Context

XSS (OWASP A03) is the most common web vulnerability. The multi-layer normalization is why ModSecurity has 8 separate normalization rules that run before each match. An attacker who knows you check for <script> will try %3Cscript%3E. An attacker who knows you URL-decode will try %253Cscript%253E (double-encoded).


Generated from week10.md · 8 Oct 2026 16:36