Unicode Normalization XSS Bypasses in Filters and Frameworks
Filters love ASCII. Real software speaks Unicode. If a denylist blocks <script> but the application later normalizes Unicode to ASCII-ish forms, your weird code points can become the forbidden string after the check.
That gap—check on string A, execute on string B—is the bug. You are not arguing aesthetics with a WAF signature. You are showing two different strings that security code treated as one.
Normalization in one paragraph
Unicode forms (NFC, NFD, NFKC, NFKD) reshuffle characters. Compatibility forms (NFKC/NFKD) are the dangerous ones for security filters: a single compatibility character may decompose into <, >, or letters that rebuild script. Homoglyphs that only look like <script> are a different issue; here you care about bytes that become syntax after normalization.
Frameworks may normalize URLs, form fields, filenames, or locale strings before rendering. WAFs may inspect the raw request while the app decodes percent-encoding and then normalizes.
Keep a short lab table: input code points, form used, output after NFKC, and whether the XSS filter saw the before or after string. That table belongs in the report; it turns a "weird payload" into an engineering bug.
Test method that stays disciplined
Pick a reflection you already understand. Confirm a plain <script> or event handler is blocked or escaped. Then introduce compatibility characters and overlong sequences where still accepted, one layer at a time:
- Fullwidth less-than and other compatibility forms that NFKC maps toward ASCII
- Ligatures or circled characters if research shows mapping into your target alphabet
- Double encoding mixed with normalization order tricks
Do not dump a thousand payloads into production. Use a local lab with the same stack when you can, then confirm two or three candidates on the target.
Watch the order of operations in code: normalize → sanitize differs from sanitize → normalize. I've seen both. The second is the classic footgun.
Framework middleware that "cleans" URL paths for routing may normalize before the XSS filter ever runs. Trace that path in source or by comparing access logs to the reflected value—the mismatch often sits one layer deeper than the template.
Proving it for bounty
Show the raw request bytes, the value after server decoding (logs or a debug reflection), and the final HTML. If you only show the browser executing script, triage may assume ordinary XSS and miss why the WAF failed. Teach them the mismatch.
Keep impact grounded. A normalization bypass that yields XSS in a stored comment is stored XSS. The Unicode angle is the root cause detail, not a separate critical class by itself.
Language and locale endpoints are frequent offenders: search boxes, slug generators, and filename displays often normalize for "pretty" URLs after a superficial security filter. Hit those paths even when the main comment form looks solid.
What teams should change
Allowlist characters where possible, normalize to NFKC before validation, use parsers instead of denylists, and keep WAF and app decoding aligned. Unit tests should include compatibility characters next to every "blocked tag" case.
Unicode bugs reward people who read how the stack decodes input. If you only fuzz ASCII, you will never see them.