Every comment on a WordPress site, every post saved by a user without the unfiltered_html capability, and a large share of plugin output passes through one function on its way to the page: wp_kses(). It’s WordPress’s HTML sanitizer, the thing that decides which tags and attributes survive. For more than twenty years it has done that job with string functions and regular expressions.
In WordPress 7.2 that changes. On October 7, 2026, core committed a new wp_kses() built on the HTML API (r64233), and it’s in 7.2 Beta 1. Core says no plugin or theme changes should be needed. That’s true for the interface. The output is a different story, and if your code stores, compares or tests sanitized HTML, it’s worth an hour on staging before December 8.
Why rebuild it
The original KSES was written before HTML5 defined how browsers parse HTML, back when much of the web’s markup was typed by hand. It was a chain of string handling and PCRE patterns that tried to recognize tags well enough to filter them.
That’s the weak spot for any sanitizer. If the sanitizer reads a piece of markup one way and the browser reads it another way, the difference is where attacks live. Text the sanitizer thinks is harmless can turn out to be a tag to the browser. This week’s WebP Express flaw, CVE-2026-97328, was exactly that kind of mismatch, in a plugin’s own bundled parser.
The HTML API, which core has been building since 6.2, parses HTML the way the HTML standard says browsers do. Putting wp_kses() on top of it means the sanitizer and the browser finally agree on what the markup is. The commit closes four long-standing tickets of parsing oddities along the way: #37698, #58921, #59310 and #63208.
How the new version works
The new implementation uses the HTML API’s Tag Processor. It walks the input token by token, mostly in a single forward pass, and builds fresh output from what it understood instead of patching the original string. It doesn’t build a full document tree, and it only runs the block parser when the content actually contains blocks.
The function signature doesn’t change. You call wp_kses( $content, $allowed_html ) the same way. During the transition, the new code lives in wp_sanitize_html_kses() next to the legacy implementation.
What comes out differently
This is the part to read carefully. The progress report by Dennis Snell lists the changes. The ones most likely to matter in practice:
- Output is normalized. Tag names become lowercase, attribute values get double quotes, duplicate attributes are dropped, the self-closing slash on void elements like
<img />goes away, and character references are decoded and re-encoded consistently. Semantically, nothing changes. Byte for byte, almost everything does. - Disallowed
<script>and<style>lose their contents. The old code stripped only the tags, leaving the JavaScript or CSS behind as visible text. Now the whole element goes. Other disallowed tags still lose just their tags and keep their content. - Stray angle brackets are kept. Text like
a < bused to be at risk of being swallowed as a broken tag. Now it’s escaped to<and kept. - Some input is rejected or cut short. Content containing
<noscript>is rejected. Some malformed comments are rejected. SVG and MathML that need more than the Tag Processor can parse with certainty end the output at that point. A partial tag at the very end, like<butto, is dropped. - Ampersands without semicolons are read the browser’s way. In text,
§ioncan now decode to§ion, because that’s what browsers do. In attributes it doesn’t. The fix is the one HTML has always asked for: write&, and end entities with a semicolon. - Unbalanced tags stay unbalanced. The new code deliberately doesn’t auto-close tags, because existing code sanitizes fragments and joins them later.
- Block delimiters are re-serialized. Follow-up commits normalize every block comment delimiter, and fixed one early bug where empty block attributes came back as an empty list. If you parse block comments with your own regex, test it.
What could break
Rendering, mostly not. Browsers already parsed the old output the way the new code does. What breaks is code that cares about the exact string:
- Unit tests that assert sanitized output equals a hard-coded string. Core had to update many of its own tests for the same reason.
- Change detection. Code that sanitizes content and compares it to the stored version, or hashes it, to decide whether something changed. After the update, everything looks changed once.
- Stored, pre-sanitized HTML that gets re-sanitized on save, producing revisions or sync events nobody made.
- Content with inline SVG or MathML saved by users without
unfiltered_html, which may now come back truncated. - Imports and migrations of old, hand-typed HTML, where the old parser’s quirks were quietly holding things together.
How to test it
On a staging copy running 7.2 Beta 1 or later, run real content through both implementations and compare. The opt-out filter makes that easy, since its stated purpose includes side-by-side testing. A small script, run with wp eval-file:
<?php
// Compare legacy and new wp_kses() output for recent posts.
$allowed = wp_kses_allowed_html( 'post' );
$posts = get_posts( array( 'numberposts' => 200, 'post_status' => 'any' ) );
foreach ( $posts as $post ) {
add_filter( 'wp_kses_force_legacy_parser', '__return_true' );
$old = wp_kses( $post->post_content, $allowed );
remove_filter( 'wp_kses_force_legacy_parser', '__return_true' );
$new = wp_kses( $post->post_content, $allowed );
if ( $old !== $new ) {
echo $post->ID . ' ' . $post->post_title . "\n";
}
}
Expect plenty of hits, because normalization alone changes most strings. For each one, the question is whether the difference matters. For a quick answer to that, compare the two outputs as HTML instead of as strings. In PHPUnit, WordPress’s assertEqualHTML() does exactly that and shows only the differences a browser would notice. For one-off snippets, there’s a browser-based tester.
Run the same comparison over comments, and over any custom post types or options where your plugin stores user-supplied HTML.
The escape hatch, and why not to lean on it
If something breaks and you need the old behavior back right now:
add_filter( 'wp_kses_force_legacy_parser', '__return_true' );
Treat it as a stopgap. Core says the filter and the legacy code will be removed in a future release, possibly as early as 7.2.0 itself if testing goes well. Code that only works with the old parser is code that depends on the sanitizer misreading HTML, which is the exact problem this rebuild fixes. The real fix is to stop depending on the exact output string.
Where this lands
The commit is in trunk and in 7.2 Beta 1. Core’s own posts still say 7.2 or 7.3, depending on what testing finds, so it could yet be held back. Beta 2 is due October 27, the release candidate on November 17, and the final release on December 8. If you find a difference that breaks something, report it on Trac #66208 with the input, the old output and the new output. That’s the most useful bug report this release can get.
For sites built the classic way, like this one, this is the most consequential change in 7.2. There are no new editor panels and nothing to see. But every piece of HTML that isn’t yours goes through it, and it just got a lot harder to fool.