Last week a critical XSS landed in SunEditor, a rich-text editor a lot of applications embed. CVE-2026-59167, CVSS 10, fixed in 2.47.11. The advisory's own one-line summary is the interesting part: sanitizer bypass.
Not "SunEditor had no sanitizer". It had one. The sanitizer ran, did its job as written, and the output still executed JavaScript.
That distinction is worth more than the CVE, because the shape of it is not specific to SunEditor and will outlive this patch.
The assumption that breaks
The mental model most of us carry is that sanitizing is filtering: you take a string of user HTML, remove the dangerous parts, and what remains is safe.
That model quietly assumes there is one agreed answer to the question "what is in this string". There is not. Sanitizing is two parsers agreeing with each other, and you only control one of them.
Your sanitizer parses the input and decides what the elements and attributes are. Later, a browser parses the sanitized output and decides what the elements and attributes are. If those two ever disagree about the same bytes, the browser wins, because the browser is the one that renders it.
Every sanitizer bypass in history is a place where they disagree.
What disagreed this time
The SunEditor advisory names it precisely: the sanitization logic "does not fully remove executable event-handler attributes from certain custom/namespaced tags", and it points at namespaced elements, written like <a:b>, as the construct involved.
You can see the shape of the mistake without the payload. A filter that works from a list of known elements has to decide what to do with an element it does not recognise. <a:b> is not in anyone's allowlist. It is not a real HTML element. A reasonable-looking implementation treats it as unknown, declines to apply the attribute rules it has for real elements, and leaves it alone.
The browser is less fussy. It parses that into an element in the DOM like any other, and an onclick attribute sitting on it is an event handler like any other. The sanitizer saw something it did not recognise; the browser saw something it was perfectly happy to run.
Notice that the sanitizer was not bypassed by a cleverer string. It was bypassed by a construct that fell outside the set of things it had opinions about.
The rule that generalises
Read the advisory's own remediation list and every item is really one instruction:
- Normalize DOM elements before sanitization
- Explicitly reject or unwrap unknown/custom/namespaced tags
- Strip all event-handler attributes from all elements, including unknown/custom elements
- Re-validate sanitized output after browser DOM parsing
The instruction is: stop sanitizing the string and start sanitizing the tree. Parse the input with the same engine that will eventually render it, walk the resulting nodes, and decide per node. Then your parser and the browser's parser cannot disagree, because there is only one of them.
And the second half, which is the part people skip: an allowlist has to be a decision about everything, not a decision about the things you listed. "Remove onclick from elements I know" leaves a hole shaped exactly like the elements you do not. "Remove every attribute beginning with on, from every node, whatever it is" does not.
Why this class reaches you badly
Stored XSS has an awkward property: the person who sees it is never the person who can fix it, and usually not even the person who caused it.
An author saves content. A reader loads the page days later and the payload fires in their browser. The developer is the third party in that transaction and observes none of it.
Your server logs are no help, because nothing went wrong on the server. The save returned 200. The page render returned 200. Both are accurate. The entire event happened after the bytes left your machine, in a browser you have no access to, belonging to somebody who has no vocabulary for what they just saw.
So it arrives as "a weird box appeared", or "the page flashed red", or most often as nothing at all, because the reader assumed their own machine was playing up and closed the tab.
That is the uncomfortable takeaway. For a whole class of serious bug, your instrumentation is structurally in the wrong place, and the only witness is someone with no idea what they witnessed and no way to hand you the evidence.
Upgrade SunEditor if you embed it. But the more durable action is to go and look at whatever sanitizes user HTML in your own stack, and find out whether it is reasoning about a string or about a tree.
Top comments (0)