# Sanitizer parser differential bypass

Sanitizer and browser parse the same bytes differently, so approved markup becomes live script

_Area: Web injection and code execution. Seen in 20 disclosed reports; 12 confirmed bounties, median $3,700, range $750-$10,000; latest 2026-10-22. Updated 2026-10-23._

## How it works
A sanitizer decides what is safe by parsing input with its own tokenizer; a browser or later decoder then parses the same bytes under different rules. The invariant that must hold: the exact bytes the sanitizer approved must parse to the same tree in the final consumer. Anywhere the two disagree — what counts as a tag, where a raw-text block ends, how an escape decodes, which namespace an element lives in — the sanitizer approves content the browser later turns into live markup or script.

## Where it shows up
- Escaping happens before a decoding step (Punycode/IDN, CSS escapes) that can emit `<`, `>`, or quotes afterward [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-splitting-the-email-atom.md)[E4](https://bugbountylibrary.com/r/h1%3A982291.md)[E5](https://bugbountylibrary.com/r/blog%3Aadico-me-post-xss-in-gmail-s-amp4email.md).
- Post-sanitization DOM mutation: renaming or moving elements changes namespace (svg/math to HTML) and re-parses children [E2](https://bugbountylibrary.com/r/blog%3Asonarsource-com-blog-code-vulnerabilities-leak-emails-in-proton-mail.md).
- Two sanitizers run in sequence with different tokenizers (strip_tags vs KSES) on edge cases like whitespace after `<` [E3](https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-xss2shell.md).
- Allowlisted elements whose content model is raw text (noscript, style); the sanitizer mishandles where that block ends [E6](https://bugbountylibrary.com/r/h1%3A2931691.md).
- Parser nesting/depth tracking desynced by unclosed tags, letting an extra tag through [E7](https://bugbountylibrary.com/r/h1%3A1675516.md).
- Custom onIgnoreTag/allowlist callbacks that return raw tag text for edge prefixes like `<![` [E8](https://bugbountylibrary.com/r/h1%3A1404804.md).

## How to review
For each parser pair (sanitizer vs browser, filter vs decoder), confirm they agree on the invariant above. Order checks by pipeline stage: first, confirm escaping is the last transformation and fuzz the decoder for malformed input that emits metacharacters post-escape [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-splitting-the-email-atom.md). Run CSS octal/hex escapes (`\00003c`) through the pipeline and confirm decoding happens before, not after, filtering [E4](https://bugbountylibrary.com/r/h1%3A982291.md)[E5](https://bugbountylibrary.com/r/blog%3Aadico-me-post-xss-in-gmail-s-amp4email.md). Diff sequential tokenizers on `< area` and whitespace after `<` [E3](https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-xss2shell.md). For namespace flips, take a sanitized `<svg>`/`<math>` subtree, rename its root to an unknown tag, and confirm the children still parse in foreign-content context rather than HTML [E2](https://bugbountylibrary.com/r/blog%3Asonarsource-com-blog-code-vulnerabilities-leak-emails-in-proton-mail.md). Flood unclosed `<p>` and test where raw-text blocks terminate [E6](https://bugbountylibrary.com/r/h1%3A2931691.md)[E7](https://bugbountylibrary.com/r/h1%3A1675516.md). Diff custom onIgnoreTag callbacks against the browser on `<![`, `<!-->` and conditional-comment syntax [E8](https://bugbountylibrary.com/r/h1%3A1404804.md).

## Variants seen
- Decode-after-escape via malformed IDN/Punycode [E1](https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-splitting-the-email-atom.md).
- Namespace flip by renaming svg to a custom tag [E2](https://bugbountylibrary.com/r/blog%3Asonarsource-com-blog-code-vulnerabilities-leak-emails-in-proton-mail.md).
- Sequential sanitizer tokenizer mismatch [E3](https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-xss2shell.md).
- CSS octal escapes inside style/url() [E4](https://bugbountylibrary.com/r/h1%3A982291.md).
- CSS hex escapes inside selectors breaking out of style [E5](https://bugbountylibrary.com/r/blog%3Aadico-me-post-xss-in-gmail-s-amp4email.md).
- noscript attribute-context breakout [E6](https://bugbountylibrary.com/r/h1%3A2931691.md).
- Unclosed-tag nesting desync [E7](https://bugbountylibrary.com/r/h1%3A1675516.md).
- Custom onIgnoreTag returning raw `<![`-prefixed text [E8](https://bugbountylibrary.com/r/h1%3A1404804.md).

## Why fixes fail
E7 shows depth tracking that a tag flood can still desync; capping depth only moves the threshold. E8 shows a custom onIgnoreTag callback that returns raw tag text, so the sanitizer's tokenizer and the browser disagree on `<![`-prefixed input and attribute injection survives. The only durable fix is one shared parser: sanitize the tree the browser will actually build, and never decode or mutate after approval.

## Impact and severity
Led to stored XSS and account takeover [E3](https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-xss2shell.md)[E4](https://bugbountylibrary.com/r/h1%3A982291.md)[E8](https://bugbountylibrary.com/r/h1%3A1404804.md); E2 exfiltrated decrypted email via a blob URL (exfiltration, not XSS itself); E5 was XSS. In E1 the CSS injection exfiltrated the admin CSRF token and chained to RCE; E3 chained XSS to RCE. RCE is chain-dependent, not inherent to the pattern. Severity rises when the differential reaches privileged contexts (admin panels, email rendering) or chains to code execution.

## Evidence
- E1: https://bugbountylibrary.com/r/blog%3Aportswigger-net-research-splitting-the-email-atom.md
- E2: https://bugbountylibrary.com/r/blog%3Asonarsource-com-blog-code-vulnerabilities-leak-emails-in-proton-mail.md
- E3: https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-xss2shell.md
- E4: https://bugbountylibrary.com/r/h1%3A982291.md
- E5: https://bugbountylibrary.com/r/blog%3Aadico-me-post-xss-in-gmail-s-amp4email.md
- E6: https://bugbountylibrary.com/r/h1%3A2931691.md
- E7: https://bugbountylibrary.com/r/h1%3A1675516.md
- E8: https://bugbountylibrary.com/r/h1%3A1404804.md
