Since its inception, WordPress has relied on the KSES1 subsystem to sanitize HTML. Its purpose has grown and changed over time, but primarily serves two purposes: correct aesthetic defects that are likely the result of typos or pasting errors; and remove security risks that might appear in untrusted inputs. The original kses library was written […]
Since its inception, WordPress has relied on the KSES1 subsystem to sanitize HTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers.. Its purpose has grown and changed over time, but primarily serves two purposes: correct aesthetic defects that are likely the result of typos or pasting errors; and remove security risks that might appear in untrusted inputs. The original kses library was written before HTML5 fully-formalized HTML parsing and at a time when it was common to “hand type” HTML. Today, however, there is no such thing as “seriously malformed2” HTML and most people author content through rich editors, such as WordPress’ Block Block is the abstract term used to describe units of markup that, composed together, form the content or layout of a webpage using the WordPress editor. The idea combines concepts of what in the past may have achieved with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. editor. This means that today, the primary purpose of wp_kses() is to prune out risks from untrusted inputs, and it’s time for it to get an overhaul.
As part of #66208, PR#13271 replaced the implementation of wp_kses() with a new one based on the HTML API. This update replaces a complicated chain of functions, string processing, PCRE regular expressions, and outdated logic to ensure that WordPress can perform its sanitization role reliably and comprehensibly.
Rebuilding wp_kses() should not require any changes to plugins or themes; it’s an in-place upgrade with no interface changes. However, as its mode of operation is being entirely overhauled, there may be significant impacts on inputs that were treated in particular ways by the legacy implementation.
For now, the original implementation is being kept around for compatibility reasons. A new filter gives site owners the opportunity to opt-out of running the replacement code. Reverting to the legacy mode is therefore as easy as adding the following line to your site. Before the full release of WordPress 7.2.0, any and all feedback on this new implementation will not only be greatly appreciated, but will help ensure a stable update for everyone.
add_filter( 'wp_kses_force_legacy_parser', '__return_true' );It’s safe to run the new code, as the new internal parser is more robust, faster, and requires less memory to run, but in case some existing code depends on the idiosyncrasies of the legacy wp_kses(), that implementation remains available for now, but it will disappear in future releases or even in WordPress 7.2.03.
The goal of the refactor is to preserve the intent of the safety and aesthetic checks in the legacy wp_kses() function while ensuring that there will never be a mis-parse of HTML. To that extent, it is hard to quantify or completely qualify the kinds of changes this will bring to bear. There are a number of common scenarios, however, that are likely to present themselves.
The new implementation starts by structurally parsing its input documents, meaning that logical choices are based on the meaning of the input text, not the text directly. Whereas the legacy wp_kses() implementation attempts to examine each token in an HTML input and make adjustments to that original document, the new implementation creates its output string as it proceeds by creating a fresh serialization of the parsed input.
This means that character references in text nodes will be fully un-escaped and deterministically re-escaped, tags will appear all-lowercase, attributes double-quoted, no ignored self-closing flags for void elements4, etc.
<!-- Input HTML -->
The <IMG class=bike class="boat" src='vehicle.png' /> & the wheels…
<!-- Legacy behavior -->
The <IMG class="bike" src="vehicle.png" /> & the wheels…
<!-- New behavior -->
The <img class="bike" src="vehicle.png"> & the wheels…The goal of these changes is to produce more predictable HTML so that other parsing code is more likely to successfully recognize the HTML it receives, even if that other code suffers the same problems characteristic with over-simplified HTML parsing. This is attempting to remove the edge cases that make handling HTML reliably so difficult.
This normalization also means that there are a number of places that content was erroneously removed before which will now be preserved. The legacy implementation took defensive measures against inputs it didn’t understand, often swallowing up content that appeared like a tag A directory in Subversion. WordPress uses tags to store a single snapshot of a version (3.6, 3.6.1, etc.), the common convention of tags in version control systems. (Not to be confused with post tags.). Since the HTML API An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. understands that structure, however, these situations are recognized and normalized to prevent this very kind of confusion.
<!-- Input HTML -->
It would have to be <$500 for me to be interested in buying it; it’s already >5yo and past its warranted life.
<!-- Legacy behavior -->
It would have to be 5yo and past its warranted life.
<!-- New behavior -->
It would have to be <$500 for me to be interested in buying it; it’s already >5yo and past its warranted life.This one is a bit esoteric, but might appear when least expected: there’s a subtlety in how HTML parsers treat named character references which are missing their closing semicolon. Some character references may be recognized without a terminating semicolon, such as ©, while others unconditionally require them, such as with ·. Things become ambiguous when there are successive characters after such a named reference which themselves do not form another character reference: e.g. ©paste; It looks like there’s a name here, but there’s no character reference named copypaste.
Inside of text nodes, these semicolon-optional character references are decoded, sometimes surprisingly, but when found inside HTML attributes they aren’t, which is yet another surprise. In the ©paste; example the rendered text would show ©paste;, but an attribute value would remain ©paste;. The reason for this discrepancy is how URL A specific web address of a website or web page on the Internet, such as a website’s URL www.wordpress.org search parameters are frequently mis-encoded in src, href and other attributes. Consider a search link: <a href="/search?q=cat+pictures¬=cartoon">; it’s clear that the intent here was to add a search parameter named not to exclude search results, but if decoded strictly, would be interpreted as /search?q=cat+pictures¬=cartoon because the ¬ character reference doesn’t require its semicolon.
This exception was made to prevent breaking the existing web and remains today in all spec-compliant HTML parsers and browsers. Where this might turn up is when text nodes contain URLs containing query args or snippets of query args. Note that § substring has been replaced by the new implementation in the following snippet with the section sign §, just as a browser would.
<!-- Input HTML -->
Add <code>/?preview=true§ion=grilling</code>.
<!-- Legacy behavior -->
Add <code>/?preview=true&section=grilling</code>.
<!-- New behavior -->
Add <code>/?preview=true§ion=grilling</code>.To avoid this situation, authors should always escape the ampersand character & when writing HTML, and always include the semicolon for character references. This is particularly true within URL query strings, as the unescaped ampersands can be easy to overlook; one must remember that the decoded value should be &, thus the attribute value must contain &.
The HTML API defines the concept of a “special atomic element.” These are essentially the elements whose content cannot contain other markup. That is, all of the bytes found inside of them are parsed as plaintext, sometimes un-escaping character references and other times not. Because of the unique rules applied to the content of each of these kinds of elements, the HTML API treats them as if they were a single unit — it provides the entire element or none at all.
In the legacy wp_kses() implementation, if a SCRIPT tag were not allowed (as is the case for the default “post” context), then the opening and closing tags would be removed but the content would stick around. This was safe behavior because the executable code was removed from its executable content, but it could leave large rendered spans of JavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser.
https://www.javascript.com code on the final page.
The new behavior is that when one of these tags is not allowed, it will be removed with all of its content too, automatically. This is not the case for other tags generally. For example, if section isn’t passed in for the allowable HTML tags, then only the opening and closing <section> and </section> tags will be removed. The nature of the special atomic elements warrants the change.
<!-- Input HTML -->
Click on my <script>alert(1)</script>!
<!-- Legacy behavior -->
Click on my alert(1)!
<!-- New behavior -->
Click on my !There’s another side of this that could be surprising: there might be content inside of these special elements that looks like a tag but isn’t. The updated code recognizes when things that look like tags but aren’t, and then escapes that content where possible to prevent further confusion from downstream code.
<!-- Input HTML -->
<title>The <sneaky> tag is not allowed.</title>
<div>The <sneaky> tag is removed.</div>
<!-- Legacy behavior -->
<title>The tag is not allowed.</title>
<div>The tag is removed.</div>
<!-- New behavior -->
<title>The <sneaky> tag is not allowed.</title>
<div>The tag is removed.</div>While wp_kses() is usually examining a snippet of HTML in isolation, its output is typically being rendered into a containing HTML document. Because of this, it’s important to guard the end of the output to make sure that it can’t form a tag when joined with other strings. The legacy implementation handles this by treating anything which fails to fully parse as plaintext and escape it, which produces safe but often corrupt outputs. The HTML API pauses when it finds text that looks like it could have been truncated, meaning that it might be the start of a tag or comment or other syntax element. Therefore the refactor avoids treating these tail-ends as if they were text and simply discards them instead.
<!-- Input -->
Click on the <butto
<!-- Legacy behavior -->
Click on the <butto
<!-- New behavior -->
Click on the For most situations the new behavior will result in less-clobbered text on the rendered page. However, there are a few points to be aware of regarding the special atomic elements. wp_kses() was built in a way that allows for un-matched tags at the start and end of the input, and plenty of code sends these kinds of incomplete fragments. For the most part, this has been preserved, but the special atomic elements are unique in that the parser changes modes inside of the elements. Therefore, if any of these elements appears incompletely, then the entire element will be removed. Complete in this context would mean a proper opening tag followed by a proper closing tag, including the contained content.
<!-- Input -->
Use the <title> tag to open a title.
<!-- Legacy behavior (assuming that TITLE is allowed ) -->
Use the <title> tag to open a title.
<!-- New behavior -->
Use the When SVG or MathML content is directly embedded in HTML it presents a complicated situation. SVG and MathML are normatively XML applications and adhere to XML’s draconian error policy5 — a parser must reject a document entirely if there is a single error or mistake in it — but when embedded into an HTML document they are a blend of HTML and XML6. When the markup behaves XML-like, it’s relatively straightforward to parse, but when it toggles into the HTML-like syntax then it becomes harder to know when to switch back or when the content ends.
Because of this, the new implementation of wp_kses() will parse as far into SVG and MathML content as it can with certainty. It’s able to know when proceeding would require further complexity7, and when it would, then it aborts entirely and returns the input up until that SVG or MathML content begins. This is important not only for wp_kses(), but safeguards downstream code and parsers which also might be unaware of the more-substantial rules.
<!-- Input -->
<li>XML-like <math><mtext>MathML</mtext></math>
<li>HTML-like <math><mtext><span>MathML</span></mtext></math>
<!-- Legacy behavior -->
<li>XML-like <math><mtext>MathML</mtext></math>
<li>HTML-like <math><mtext><span>MathML</span></mtext></math>
<!-- New behavior -->
<li>XML-like <math><mtext>MathML</mtext></math>
<li>HTML-likeThe availability of an HTML parser brings capabilities A capability is permission to perform one or more types of task. Checking if a user has a capability is performed by the current_user_can function. Each user of a WordPress site might have some permissions but not others, depending on their role. For example, users who have the Author role usually have permission to edit their own posts (the “edit_posts” capability), but not permission to edit other users’ posts (the “edit_others_posts” capability). that weren’t previously possible, and in some cases, there are challenging decisions on what to allow based on those capabilities. The new implementation of wp_kses() applies some hard-limits in situations where the legacy implementation didn’t. This includes rejecting various kinds of malformed HTML comments, documents containing the NOSCRIPT element, and various elements containing confusable content.
In certain risky situations, whereby the risk is high that downstream parsers might get confused, or where the major browsers currently disagree on how to interpret the specification, elements may be rewritten in another form. For example, CDATA sections and PLAINTEXT element contents are transformed into normal escaped text.
It’s easy to point out issues with wp_kses(), but it’s much harder to classify if those issues warrant change or not. There is essentially one value leading the decisions in these cases: don’t make wp_kses() worse than it was. This manifests in a couple of ways: for normative runs it should reproduce the existing normative behaviors; but for cases where expectations are already broken, there is no need to retain those broken behaviors. These decisions are also made against the backdrop of the security-sensitive role that wp_kses() plays — its purpose is to prevent misuse, so some changes will result from diligence in hardening despite a regretful change in behavior8.
There are some debatable historic decisions within wp_kses(). One, for instance, is preserving so-called “unbalanced tags” in the input. One might expect wp_kses() to add missing closing tags to its input, but plenty of existing code in the directory and in Core already passes “unbalanced” HTML intentionally through the function and expects it to remain unbalanced in the output. This is because people sometimes concatenate chunks of HTML which have been separately-sanitized. This may not be the ideal way to accomplish the task, but it’s well-established.
This is a situation where the existing behavior is preserved. Adding those missing tags was explored, but ultimately rejected for this reason.
Allow already-broken behaviors to change.It’s also up for debate what “broken” exactly means, but for the sake of this refactor, and all refactors with the HTML API, the qualification is whether a broken behavior was expected or not. For instance, wp_kses() has generally not been aware of the special parsing rules inside an HTML TITLE element. Consider its behavior when img is and isn’t allowed.
<?php
echo wp_kses(
'<title><img></title>',
array( 'title' => true, 'img' => array( 'src' => array( 'required' => true ) ) )
);
// <title><img></title>
echo wp_kses(
'<title><img /></title>',
array( 'title' => true, 'img' => array( 'src' => array( 'required' => true ) ) )
);
// <title></title>
echo wp_kses( '<title><img></title>', array( 'title' => true ) );
// <title></title>There is no real way to intuit the results of these calls because the parser was confused on the nature of the <img> and <img /> byte sequences. Every one of the outputs of these calls is wrong because no IMG element exists within the TITLE.
<?php
add_filter( 'wp_kses_force_legacy_parser', '__return_false' );
echo wp_kses(
'<title><img></title>',
array( 'title' => true, 'img' => array( 'src' => array( 'required' => true ) ) )
);
// <title><img></title>The proper way to sanitize and normalize content within the TITLE element is to escape it as text, after first decoding any character references inside it. This behavior was changed because there’s no possible way to say that the existing behavior was correct without creating internal inconsistencies. It might have been “correct” in some cases for some people, but an adjustment here only changes who is affected by the inconsistent state and it’s not viable to attempt to evaluate whose broken flows are more or less important.
So this change was adopted because it establishes clear expectations for everyone and fixes situations in which it was not previously possible for any consistent notion of “it works.”
ConclusionPlease give the new implementation of wp_kses() a try and share any feedback you have in the ticket Created for both bug reports and feature development on the bug tracker. (#66208), or leave a comment below. Expect this to be part of WordPress 7.2 or 7.3 dependent on what this period of testing turns up. Thank you for your attention and participation!
Props to @jonsurrell and @westonruter who reviewed this post before publishing.
kses library. ↩︎wp_kses() implementation will disappear is directly related to the severity The seriousness of the ticket in the eyes of the reporter. Generally, severity is a judgment of how bad a bug is, while priority is its relationship to other bugs. of issues uncovered in testing. If all goes well, WordPress 7.2.0 could include a wholesale replacement of the legacy function without any transition support in the released version. ↩︎wp_kses() implementation relies on the Tag Processor, which can parse every tag, comment, text node, etc., but which isn’t aware of the tree-structure of an HTML document. The HTML Processor, its more-powerful counterpart, is able to build that tree, but this process involves considerably more time and effort. ↩︎wp_kses() is meant to sanitize untrusted inputs. There is a certain blurriness in how specific inputs should be handled in order to neutralize potential dangers. Because of this, there are intentionally fewer behavioral guarantees and the logic will lean conservatively. That is, it will be safety to regretfully reject some safe inputs in order to ensure that more unsafe inputs are rejected. ↩︎| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Call for Code Review Help: Repairing Gutenberg GitHub Actions Workflows for wp/x.y Branches | 0 | 8.13 | 16-10-2026 |
| 2 | Функция WordPress: wp_add_crossorigin_attributes() | 0 | 19.09 | 20-08-2026 |
| 3 | WordPress Developer | 0 | 6.22 | 13-08-2026 |
| 4 | WordPress : valider le code HTML des meta tags et oEmbeds | 0 | 9.41 | 22-07-2026 |
| 5 | WordPress-Lücke: Login-Seite öffnet Tür zur Serverübernahme | 0 | 41.25 | 12-08-2026 |
| 6 | Cybersecurity for WordPress: Protecting Websites from Next-Gen Threats #wordpress #internet #cybersecurity | 0 | 14.4 | 15-06-2026 |
| 7 | heise+ | Datenlecks auf WordPress-Websites finden und abdichten | 0 | 16.9 | 27-07-2026 |
| 8 | automattic/blocks-engine-php-transformer | 0 | 17.14 | 03-08-2026 |
| 9 | The Wirth of Processing with WordPress’ Streaming APIs | 0 | 6.46 | 10-07-2026 |