跳到正文
原文
Make WordPress Core(核心开发)· Dennis Snell·· 21 天前精选编辑评分76

WordPress 推进 wp_kses() 基于 HTML API 的核心重构

Progress Report: wp_kses()

编辑导读

WordPress 正以基于 HTML API 的新实现重构 wp_kses,在不改变插件和主题接口的前提下替换旧有字符串处理与 PCRE 逻辑。新实现会规范化 HTML、解析歧义字符引用、移除不允许的 SCRIPT 与 STYLE 内容,并拒绝部分复杂 SVG 和 MathML;站点可通过 `wp_kses_force_legacy_parser` 过滤器暂时启用旧实现。

推荐理由

文章详细对比 wp_kses() 新旧解析行为,涵盖 HTML 规范化、特殊元素、SVG 与 MathML 及兼容迁移方式。

自诞生以来,WordPress 一直依赖 KSES1 子系统来清理 HTML。它的用途随着时间不断扩展和变化,但主要有两个目的:修正很可能由拼写错误或粘贴错误造成的美观缺陷;以及消除可能出现在不受信任输入中的安全风险。最初的 kses 库编写于 HTML5 完全规范 HTML 解析之前,当时“手动输入”HTML 也很常见。然而如今,已经不存在所谓“严重格式错误2”的 HTML,大多数人通过富文本编辑器(例如 WordPress 的区块编辑器)创作内容。这意味着,如今 wp_kses() 的主要用途是剔除不受信任输入中的风险,而它也到了需要彻底翻新的时候了。

作为 #66208 的一部分,PR#13271 用一套基于 HTML API 的新实现替换了 wp_kses() 的原有实现。此次更新替换了复杂的函数链、字符串处理、PCRE 正则表达式和过时逻辑,以确保 WordPress 能够可靠且清晰易懂地履行其清理职责。

哪些变化可能会影响现有插件、主题或 Core 代码?

重建 wp_kses() 不应要求插件或主题做任何更改;这是一次原位升级,接口没有变化。不过,由于其运行方式正在被彻底翻新,旧版实现以特定方式处理的输入可能会受到重大影响。

一个选择退出过滤器可保留现有行为。

目前,出于兼容性考虑,旧版实现仍被保留。新增的过滤器让站点所有者可以选择不运行替代代码。因此,只需在站点中添加以下一行,即可恢复到旧版模式。在 WordPress 7.2.0 正式发布之前,我们不仅会非常感激有关这项新实现的各种反馈,这些反馈还将帮助确保everyone都能获得稳定的更新。

add_filter( 'wp_kses_force_legacy_parser', '__return_true' );

运行新代码是安全的,因为新的内部解析器更稳健、更快速,运行时所需的内存也更少;但如果某些现有代码依赖旧版 wp_kses() 的特殊行为,该实现目前仍然可用,但它将在未来的版本中消失,甚至可能在 WordPress 7.2.03 中消失。

这次重构会如何改变该函数的行为?

此次重构的目标是保留旧版 wp_kses() 函数中安全检查和美观检查的意图,同时确保永远不会错误解析 HTML。就这一点而言,很难量化或完全界定此次改动可能带来的变化类型。不过,有许多常见情形很可能会出现。

文本节点和所有语法标记都会标准化。

新实现首先对输入文档进行结构化解析,这意味着逻辑选择依据的是输入文本的含义,而非文本本身。旧版 wp_kses() 实现会尝试检查 HTML 输入中的每个标记,并对原始文档作出调整;新实现则在对解析后的输入进行全新序列化以生成输出字符串的同时,逐步构建输出。

这意味着文本节点中的字符引用将被完全反转义并进行确定性的重新转义,标签将全部以小写显示,属性将使用双引号,void 元素4 将不再带有被忽略的自闭合标记,等等。

<!-- Input HTML -->
The <IMG class=bike class="bo&#x0061;t" src='vehicle.png' /> & the wheels&hellip;

<!-- Legacy behavior -->
The <IMG class="bike" src="vehicle.png" /> &amp; the wheels&hellip;

<!-- New behavior -->
The <img class="bike" src="vehicle.png"> &amp; the wheels…

这些更改的目标是生成更可预测的 HTML,从而让其他解析代码更有可能成功识别所收到的 HTML,即使这些代码本身也受到过度简化的 HTML 解析所带来的典型问题影响。此举旨在消除那些使可靠处理 HTML 变得非常困难的边缘情况。

这种规范化还意味着,过去有 numerous 个位置的内容被错误移除,而现在会被保留。旧版实现会采取防御性措施处理其无法理解的输入,往往会吞掉看起来像标签的内容。然而,由于 HTML API 理解这种结构,这些情况现在会被识别并规范化,从而防止正因如此引发的混淆。

<!-- 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 &lt;$500 for me to be interested in buying it; it’s already &gt;5yo and past its warranted life.

有歧义的与号会被解析。

这一点有点深奥,但可能在意想不到的地方出现:HTML 解析器处理缺少结尾分号的 命名字符引用 时存在一个微妙之处。有些字符引用可以在没有终止分号的情况下被识别,例如 &copy;而另一些则无条件要求必须有分号,例如 &middot;。当此类命名引用之后的后续字符本身不构成另一个字符引用时,情况就会变得有歧义:例如 &copypaste; 看起来这里有一个名称,但并不存在名为 copypaste 的字符引用。

在文本节点内部,这些可选带分号的字符引用会被解码,有时结果会出人意料;但当它们出现在 HTML 属性内部时则不会被解码,这又是一件出人意料的事。在 &copypaste; 示例中,渲染后的文本会显示 ©paste;,但属性值仍会保留为 &copypaste;。之所以存在这种差异,是因为 URL 搜索参数在 src、href 和其他属性中经常被错误编码。考虑以下搜索链接:<a href="/search?q=cat+pictures&not=cartoon">;显然,这里的意图是添加一个名为 not 的搜索参数,以排除搜索结果,但如果严格解码,则会被解释为 /search?q=cat+pictures¬=cartoon,因为 &not 字符引用不需要分号。

设置这一例外是为了避免破坏现有 Web,并且直到今天仍保留在所有符合规范的 HTML 解析器和浏览器中。它可能出现在 文本节点包含带有查询参数或查询参数片段的 URL 时。请注意,在下面的代码片段中,&sect 子字符串已由新实现替换为章节符号 §,其方式与浏览器相同。

<!-- Input HTML -->
Add <code>/?preview=true&section=grilling</code>.

<!-- Legacy behavior -->
Add <code>/?preview=true&amp;section=grilling</code>.

<!-- New behavior -->
Add <code>/?preview=true§ion=grilling</code>.

为避免这种情况,作者在编写 HTML 时应始终转义与号字符 &,并且始终在字符引用中包含分号。在 URL 查询字符串中尤其如此,因为未转义的与号很容易被忽略;必须记住,解码后的值应为 &,因此属性值必须包含 &amp;。

SCRIPT 和 STYLE 元素的内容会连同其相应标签一起被移除。

HTML API 定义了“特殊原子元素”这一概念。它们本质上是内容不能包含其他标记的元素。也就是说,在其中发现的所有字节都会被解析为纯文本,有时会反转义字符引用,有时则不会。由于适用于这类元素各自内容的规则各不相同,HTML API 会将它们视为一个整体——要么提供整个元素,要么完全不提供。

在旧版 wp_kses() 实现中,如果不允许某个 SCRIPT 标签(默认的“post”上下文就是这种情况),那么开始和结束标签会被移除,但内容会保留下来。这种行为是安全的,因为可执行代码已从可执行内容中移除,但最终页面上可能会残留大段渲染出来的 JavaScript 代码。

新行为是,当其中某个标签不被允许时,它及其全部内容也会自动一并移除。一般的其他标签并非如此。例如,如果没有为允许的 HTML 标签传入 section,那么只会移除 <section> 和 </section> 开始与结束标签。特殊原子元素的性质使这一变更加合理。

<!-- Input HTML -->
Click on my <script>alert(1)</script>!

<!-- Legacy behavior -->
Click on my alert(1)!

<!-- New behavior -->
Click on my !

这件事还有另一面可能令人意外:这些特殊元素内部可能存在看起来像标签、但实际上并非标签的内容。更新后的代码能够识别出这些看似标签却不是标签的内容,然后在可能的情况下对其进行转义,以防后续代码产生更多混淆。

<!-- 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 &lt;sneaky&gt; tag is not allowed.</title>
<div>The  tag is removed.</div>

不完整的输入会在歧义开始之前被截断。

虽然 wp_kses() 通常会单独检查一段 HTML 片段,但其输出通常会被渲染到一个包含它的 HTML 文档中。因此,保护输出末尾以确保它与其他字符串连接后不会形成标签,这一点很重要。旧版实现会将任何无法完整解析的内容视为纯文本并进行转义,这虽然能保证安全,但往往会生成损坏的输出。HTML API 在发现看起来可能被截断的文本时会暂停,因为它可能是标签、注释或其他语法元素的开始。因此,这次重构避免把这些尾部内容当作文本处理,而是直接将其丢弃。

<!-- Input -->
Click on the <butto

<!-- Legacy behavior -->
Click on the &lt;butto

<!-- New behavior -->
Click on the 

在大多数情况下,新行为会让渲染页面上的文本少一些被破坏。不过,对于特殊原子元素,仍有几点需要注意。wp_kses() 的构建方式允许输入的开头和结尾存在未匹配标签,而且大量代码会发送这类不完整的片段。总体而言,这一行为得到了保留,但特殊原子元素的独特之处在于,解析器会在元素内部切换模式。因此,如果其中任何一个元素出现不完整的情况,整个元素都会被移除。在此语境中,完整意味着一个正确的开始标签后跟一个正确的结束标签,并包含其中的内容。

<!-- 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 

需要更复杂解析的 SVG 和 MathML 会被拒绝。

当 SVG 或 MathML 内容直接嵌入 HTML 时,会形成一种复杂的情况。SVG 和 MathML 在规范上是 XML 应用,并遵循 XML 严苛的错误处理策略5——解析器必须在文档中存在任何单一错误或问题时都完全拒绝该文档——但嵌入 HTML 文档后,它们便成为 HTML 与 XML 的混合体6。当标记的行为类似 XML 时,解析相对直接;但当它切换到类似 HTML 的语法时,就更难判断何时应切回,或内容在哪里结束。

因此,wp_kses() 的新实现会尽可能深入解析 SVG 和 MathML 内容,直到能够确定解析结果为止。它能判断何时继续处理将需要更多复杂性7;一旦需要更多复杂性,它就会完全中止,并返回截至该 SVG 或 MathML 内容开始之前的输入。这不仅对 wp_kses() 很重要,还可以保护下游代码和解析器,因为它们也可能并不了解这些更为复杂的规则。

<!-- 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-like

有些输入永远不会被信任。

HTML 解析器的可用性带来了此前无法实现的能力,而在某些情况下,还需要根据这些能力就是否允许作出艰难决定。wp_kses() 的新实现在旧实现未设置硬性限制的情况下应用了一些硬性限制。其中包括拒绝各种格式错误的 HTML 注释、包含 NOSCRIPT 元素的文档,以及包含易混淆内容的各种元素。

在某些存在风险的情况下,如果下游解析器很可能感到困惑,或者各主流浏览器目前对如何解释规范存在分歧,元素可能会被改写为另一种形式。例如,CDATA 部分和 PLAINTEXT 元素内容会被转换为普通的转义文本。

促成行为变化的理由是什么?

指出 wp_kses() 中的问题很容易,但要判断这些问题是否值得修改则困难得多。在这些情况下,做决定基本上只有一个准则:不要让 wp_kses() 比原先更糟。这体现在两个方面:对于符合规范的运行,它应重现现有的规范行为;但对于预期已经失效的情况,则无需保留这些失效行为。作出这些决定时,还需考虑到 wp_kses() 所承担的安全敏感角色——其目的是防止滥用,因此,即使行为变化令人遗憾,出于加强安全性的审慎考虑,仍会进行一些修改8。

保留有意为之的行为。

wp_kses() 中存在一些值得商榷的历史决定。例如,其中一项是保留输入中所谓的“不平衡标签”。人们可能会期望 wp_kses() 为其输入补上缺失的闭合标签,但目录和 Core 中已有大量代码有意将“不平衡”的 HTML 传入该函数,并期望输出仍保持不平衡。这是因为人们有时会将分别进行过清理的 HTML 片段拼接在一起。这或许不是完成该任务的理想方式,但这是一种成熟的做法。

在这种情况下,现有行为会得到保留。我们曾探索过补上那些缺失标签的做法,但最终出于上述原因而予以否决。

允许已经失效的行为发生变化。

“失效”的确切含义仍有争议,但就此次重构以及 HTML API 的所有重构而言,判断标准是这种失效行为是否属于预期。例如,wp_kses() 通常并不了解 HTML TITLE 元素内部的特殊解析规则。请考虑 img 允许与不允许时的行为。

<?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>

由于解析器无法确定 <img> 和 <img /> 字节序列的性质,因此实际上无法凭直觉判断这些调用的结果。这些调用的每一个输出都是错误的,因为 TITLE 中不存在 IMG 元素。

<?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>&lt;img&gt;</title>

在 TITLE 元素内清理和规范化内容的正确方式,是先解码其中的任何字符引用,然后将其作为文本转义。这一行为之所以改变,是因为如果不造成内部矛盾,就无法认定现有行为是正确的。它在某些情况下对某些人而言或许曾“正确”,但这里的调整只会改变受不一致状态影响的人,而试图评判谁的错误流程更重要或次要并不可行。

因此,这项更改之所以被采用,是因为它为所有人建立了清晰的预期,并修复了此前无法以任何一致的“它能正常工作”标准来衡量的情况。

结论

请试用 wp_kses() 的新实现,并在工单(#66208)中分享你的任何反馈,或在下方留言。根据这段测试期的结果,预计它将成为 WordPress 7.2 或 7.3 的一部分。感谢你的关注与参与!


感谢 @jonsurrell 和 @westonruter 在本文发布前进行了审阅。

  1. “kses 这个名称源自 XSS 和 access 这两个词。”——见于 kses 库 0.1.0 版本的 README。↩︎
  2. HTML 实时标准规定了如何解析各种格式错误的输入,这意味着每个符合规范的解析器都应对任何给定输入产生相同的输出。放弃毫无意义,文档的解析方式也不存在宽松处理。↩︎
  3. 旧版 wp_kses() 实现消失的速度,直接取决于测试中发现问题的严重程度。如果一切顺利,WordPress 7.2.0 可能会在发布版本中完全替换旧版函数,而不提供任何过渡支持。↩︎
  4. 所谓的 XHTML 兼容 HTML 其实是个神话,这种互操作性和安全感只是表面上的。将 HTML 作为 XHTML 交付并不安全,也不存在一种通用方法,能够创建在这两套系统下都兼容的单一文档。HTML 元素上错误使用的自闭合标志最初是为了尝试生成 XHTML 安全的 HTML,但它并不符合规范,而且它的存在延续了一种误解,因为 HTML 没有自闭合元素。↩︎
  5. XML 的“严苛错误处理”也被称为“惩罚无辜的终端用户”,其设计目的是让 XML 解析器的实现保持简单。↩︎
  6. 在 HTML5 标准化期间,指导原则之一是“不要破坏互联网”。由于已有足够多的网站将带有错误的 SVG 和 MathML 粘贴到 HTML 页面中,而且当时的浏览器无论如何都会解析并显示这些内容,因此规范必须作出调整,以继续允许这些页面在符合规范的浏览器中呈现。事实上,这种最大程度向后兼容的要求,正是 HTML 大部分解析复杂性的根源。↩︎
  7. 新的 wp_kses() 实现依赖于 Tag Processor,它能够解析每个标签、注释、文本节点等,但并不了解 HTML 文档的树结构。它功能更强的对应工具 HTML Processor 能够构建该树结构,但这一过程需要多得多的时间和精力。↩︎
  8. 值得强调的是,wp_kses()旨在净化不受信任的输入。至于应如何处理特定输入以消除潜在危险,存在一定程度的模糊性。因此,它有意提供较少的行为保证,并且其逻辑会采取保守策略。也就是说,出于安全考虑,宁可遗憾地拒绝某些安全输入,也要确保拒绝更多不安全输入。↩︎

#html-api、#html-api-progress-report、#progress-report

来源:Make WordPress Core(核心开发) · https://make.wordpress.org/core/2026/10/07/progress-report-wp_kses/

部分链接为推广/关联链接。