# Sanitizer normalization differential

Sanitizer runs before a decode/normalize step, or filter and renderer normalize characters differently

_Area: Web injection and code execution. Seen in 3 disclosed reports; latest 2026-09-24. Updated 2026-10-24._

## How it works
A filter decides what is dangerous by looking at the input as it arrives. A later layer — a browser, a Windows codepage, a URL parser, an entity decoder — normalizes that same input differently, so characters the filter judged harmless become markup or executable syntax downstream. The broken assumption is that "what the filter sees" equals "what the renderer executes." Any decode, best-fit mapping, escape-stripping, or entity conversion between the check and the sink reopens the gap.

## Where it shows up
- A sanitizer (strip_tags, blacklist, WAF regex) followed by html_entity_decode, URL unescaping, or charset conversion before storage or render.
- Filters matching literal `<` but not full-width, small-form, CJK, or miscellaneous-technical angle brackets.
- Windows/codepage stacks that apply Unicode best-fit matching to input.
- Parsers that strip invalid escapes (e.g. `%g`) so a parsed key differs from the delivered key.
- Templates rendering stored content with `|raw` or equivalent, especially admin-only views.
- Control characters or stray `%` producing anomalous unescaped output.

## How to review
Trace the full path from input to sink and list every transformation in order. The invariant: no decode, normalize, or charset-conversion step may run after the last sanitization and before the sink. If a decode exists, sanitization must run after it, on the final form the renderer will interpret. Check that the filter's character model matches the renderer's (Unicode, entities, escapes). Review admin render paths separately from public ones — they often skip escaping [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-pgwp-vc7q-cvj3.md). For parser-based defenses, confirm the key the filter validates is byte-identical to the key the consumer uses [E2](https://bugbountylibrary.com/r/h1%3A134546.md).

## Variants seen
- Unicode best-fit normalization on a Windows codepage turning lookalike angle brackets into `<` after a WAF passed them [E1](https://bugbountylibrary.com/r/h1%3A309531.md).
- Control characters and `%` confusing a `<.*` regex into emitting unescaped output [E1](https://bugbountylibrary.com/r/h1%3A309531.md).
- Flash stripping invalid URL escapes so a parsed parameter key no longer matches the delivered key, defeating a scrub [E2](https://bugbountylibrary.com/r/h1%3A134546.md).
- strip_tags leaving HTML entities that a later html_entity_decode converts into live tags, stored and rendered raw in an admin editor [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-pgwp-vc7q-cvj3.md).

## Impact and severity
Stored XSS reaching admin surfaces, enabling account takeover [E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-pgwp-vc7q-cvj3.md); stored XSS on a high-traffic social platform via WAF bypass [E1](https://bugbountylibrary.com/r/h1%3A309531.md); reflected XSS in a bundled WordPress component [E2](https://bugbountylibrary.com/r/h1%3A134546.md). Severity rises when the sink is an admin view, when the payload persists, and when the bypass defeats a defense the team trusts.

## Why fixes fail
Blacklists and pre-decode sanitizers address the observed character, not the normalization step; the same class recurs with a different lookalike or a different decode order [E1](https://bugbountylibrary.com/r/h1%3A309531.md)[E3](https://bugbountylibrary.com/r/ghsa%3AGHSA-pgwp-vc7q-cvj3.md).

## Evidence
- E1: https://bugbountylibrary.com/r/h1%3A309531.md
- E2: https://bugbountylibrary.com/r/h1%3A134546.md
- E3: https://bugbountylibrary.com/r/ghsa%3AGHSA-pgwp-vc7q-cvj3.md
