How HTML Sanitizers Work

▶ Watch (1:02)

Sanitizers take untrusted HTML input from users, parse it into a DOM tree, iterate over that tree to strip dangerous elements (like event handlers that execute JavaScript), and return cleaned markup. They’re highly configurable: an application that wants to allow images but not scripts can say so. The problem is that sanitizers are software, and software has bugs. A bypass in a widely-used third-party sanitizer doesn’t affect one application; it affects every application that depends on it.

The HTML5 Parsing Gap

▶ Watch (11:21)

HTML5, introduced a decade ago, changed how browsers parse markup in ways older parsers never anticipated. One example: in HTML4, a comment ends with -->. HTML5 allows additional closing sequences. An attacker who crafts input that looks like a comment under HTML4 rules can construct a payload that terminates early under HTML5 rules, exposing a hidden image tag. A PHP parser built on libxml2, which never claimed HTML5 support, sees only a comment and strips nothing. The browser renders the XSS payload.

Why Server-Side Parsing Always Falls Short

▶ Watch (14:20)

Even with HTML5-compliant parsers, server-side sanitization has a structural problem. The server parses input, sanitizes it, serializes the clean DOM back to a string, and sends it over the network. The client then parses that string again. Parsing the same string twice, even with the same parser, can produce different DOM trees. This is called browser parsing round trips, and it’s intentional, documented behavior in the HTML spec. Different browsers, browser versions, and parsing contexts compound the gap. There are too many points of failure.

A Cascade of Vulnerable Libraries

▶ Watch (18:27)

Nizry disclosed the differential up the stack: libxml2 said it never claimed HTML5 support; PHP added a documentation warning; a third-party HTML5-claiming parser said unsafe usage was outside its control. Among sanitizers, dom-sanitizer (used in Grav CMS) closed the advisory without fixing the payloads. HTML Purifier argued that allowing comments is the user’s choice. Magento 2 called the differential non-actionable. Only TYPO3 fixed the issue and assigned a CVE. Potentially many more PHP sanitizers remain vulnerable.

The Browser-Native Fix

▶ Watch (23:47)

DOMPurify is today’s best client-side sanitization library. It still has bugs, including a recent bypass. The long-term answer is the browser-native Sanitizer API, a client-side sanitization interface built directly into Chrome and Firefox. Because the same browser engine that sanitizes also renders, there is no parser differential. At the time of the talk, it sits behind a feature flag in Firefox and is still in development in Chrome. Once it ships without a flag, moving off server-side sanitization gets much simpler.

Notable Quotes

parsing HTML is hard it’s very difficult Yaniv Nizry · ▶ Watch (14:23)

you can never guarantee that how you parse Yaniv Nizry · ▶ Watch (17:30)

there just too many points of failure Yaniv Nizry · ▶ Watch (19:59)

Key Takeaways

  • Server-side HTML sanitization fails because parsing contexts differ between server and victim browser.
  • A single parsing differential in libxml2 made every PHP sanitizer built on it bypassable with the same payloads.
  • The browser-native Sanitizer API eliminates parser differentials by sanitizing inside the same engine that renders.

About the Speaker(s)

Yaniv Nizry is a Vulnerability Researcher at SonarSource. He started as a software engineer and shifted to security work during his service in the IDF’s 8200 unit. His research focuses on finding and fixing vulnerabilities in complex systems.