剖析 WordPress `wp_kses()` 函数的源码:如何通过白名单机制过滤不安全的 HTML。

各位观众老爷,早上好、中午好、晚上好! 欢迎来到“老码农的碎碎念”系列讲座。 今天咱们要聊的是WordPress里一个非常重要的函数:wp_kses()。 别看这名字怪里怪气的,它可是WordPress安全的一道重要防线,专门用来过滤HTML的。 你可能会问,为什么要过滤HTML? 难道我们辛辛苦苦写的代码,还不配让它显示出来吗? 哎,还真不配!

HTML安全:一场永无止境的猫鼠游戏

想象一下,如果WordPress不对HTML进行任何过滤,那将会发生什么? 坏人们就可以在评论里、文章里甚至用户名里,偷偷塞一些恶意代码,比如 JavaScript。 这些 JavaScript 就像小偷一样,可能会偷走用户的Cookie、重定向用户到钓鱼网站,甚至直接控制你的网站! 这可不是闹着玩的,轻则损失用户数据,重则网站被黑,名誉扫地。

所以,为了保护网站的安全,WordPress必须对HTML进行严格的过滤。 wp_kses() 就是承担这项重任的“安全卫士”。 它的核心思想很简单:白名单机制

什么是白名单机制?

简单来说,白名单机制就是“允许什么,禁止什么”。 wp_kses() 维护着一份允许使用的 HTML 标签、属性和协议的清单。 只有出现在这份清单上的内容,才能被保留;其他的一律被干掉! 这就像一个严格的门卫,只允许持有特定通行证的人进入,其他人统统挡在门外。

wp_kses() 的基本用法

wp_kses() 函数的基本用法非常简单:

$safe_html = wp_kses( $string, $allowed_html, $allowed_protocols );
  • $string: 你要过滤的HTML字符串。
  • $allowed_html: 一个数组,定义了允许使用的HTML标签和属性。
  • $allowed_protocols: 一个数组,定义了允许使用的URL协议(比如 http, https, mailto)。

返回值 $safe_html 就是经过 wp_kses() 过滤后的安全HTML字符串。

$allowed_html:白名单的核心

$allowed_htmlwp_kses() 的灵魂所在。 它是一个多维数组,每一项代表一个允许使用的HTML标签,以及该标签允许使用的属性。

举个例子,如果我们只想允许使用 <a> 标签,并且只允许使用 href, title 两个属性,那么 $allowed_html 应该这样写:

$allowed_html = array(
    'a' => array(
        'href' => true,
        'title' => true,
    ),
);

这里:

  • 'a' 是标签名。
  • 'href' => true 表示允许使用 href 属性,true 表示不限制 href 属性的值。
  • 'title' => true 表示允许使用 title 属性,true 表示不限制 title 属性的值。

如果我们要允许使用 <img> 标签,并且允许使用 src, alt, width, height 属性,并且对 src 属性的值进行更严格的限制(比如,只允许 httphttps 协议),那么可以这样写:

$allowed_html = array(
    'img' => array(
        'src'    => true,
        'alt'    => true,
        'width'  => true,
        'height' => true,
    ),
);

$allowed_protocols:URL协议的卫士

$allowed_protocols 数组用于指定允许使用的URL协议。 默认情况下,WordPress允许的协议包括 http, https, ftp, mailto, news, irc, gopher, nntp, telnet, mms, rtsp, svn, tel, fax, xmpp 等。

如果你想自定义允许的协议,可以这样写:

$allowed_protocols = array( 'http', 'https', 'mailto' );

这样,wp_kses() 就只会允许 http, https, mailto 这三种协议的URL。

深入 wp_kses() 源码

光说不练假把式。 咱们现在就来扒一扒 wp_kses() 的源码,看看它到底是怎么工作的。 友情提示:源码略长,请准备好你的咖啡和小零食。

wp_kses() 函数位于 wp-includes/kses.php 文件中。 它的主要流程如下:

  1. 准备工作: 初始化一些变量,例如 $allowed_html$allowed_protocols
  2. 预处理: 对输入的HTML字符串进行一些预处理,例如将所有标签转换为小写。
  3. 循环处理: 遍历HTML字符串,查找标签和属性。
  4. 标签过滤: 如果找到了一个标签,就检查它是否在 $allowed_html 中。 如果不在,就直接删除该标签。
  5. 属性过滤: 如果标签在 $allowed_html 中,就检查它的每个属性是否在 $allowed_html 中。 如果不在,就删除该属性。 还会对属性的值进行进一步的检查,例如检查URL协议是否在 $allowed_protocols 中。
  6. 递归处理: 如果标签包含子标签,就递归调用 wp_kses() 函数来处理子标签。
  7. 后处理: 对处理后的HTML字符串进行一些后处理,例如将一些特殊字符转换为HTML实体。

下面是一些关键代码片段的解读:

1. 预处理:将标签转换为小写

$string = preg_replace_callback( '%<(/?)([^> ]*)( |/|>)%', 'kses_strtolower', $string );

这段代码使用正则表达式,将HTML标签转换为小写。 这样做是为了避免大小写不一致导致的安全问题。 例如,<A><a> 应该被视为同一个标签。

2. 标签过滤:判断标签是否在白名单中

if ( ! isset( $allowed_html[ $tagname ] ) ) {
    $string = str_replace( '<' . $origtag, '&lt;' . $origtag, $string );
    $string = str_replace( '</' . $origtag, '&lt;/' . $origtag, $string );
    continue;
}

这段代码判断标签 $tagname 是否在 $allowed_html 数组中。 如果不在,就将该标签转换为HTML实体,从而使其失效。 例如,<script> 标签会被转换为 &lt;script&gt;

3. 属性过滤:判断属性是否在白名单中

if ( ! isset( $allowed_html[ $tagname ][ $attrname ] ) ) {
    $string = str_replace( $whole_attribute, '', $string );
    continue;
}

这段代码判断属性 $attrname 是否在 $allowed_html 数组中。 如果不在,就直接删除该属性。

4. 属性值过滤:检查URL协议

if ( 'href' === $attrname || 'src' === $attrname || 'cite' === $attrname ) {
    $string = kses_bad_protocol( $string, $allowed_protocols );
}

这段代码针对 href, src, cite 等属性,调用 kses_bad_protocol() 函数来检查URL协议。 kses_bad_protocol() 函数会遍历 $allowed_protocols 数组,判断URL协议是否在允许的列表中。 如果不在,就将该URL替换为空字符串。

一些需要注意的地方

  • wp_kses() 并不是万能的。 它只能过滤已知的恶意代码。 如果有新的攻击方式出现,wp_kses() 可能无法有效地防御。
  • wp_kses() 的性能并不是很高。 因为它需要遍历整个HTML字符串,并进行大量的字符串操作。 因此,在处理大量HTML数据时,应该尽量避免频繁调用 wp_kses() 函数。
  • wp_kses() 的白名单配置需要根据实际情况进行调整。 如果你的网站需要使用一些特殊的HTML标签或属性,你需要将它们添加到 $allowed_html 数组中。

代码示例:自定义白名单

假设我们想创建一个允许用户输入简单的加粗和斜体文本的表单。我们可以这样自定义白名单:

function my_kses_allowed_html( $allowed_html, $context ) {
  if ( 'my_custom_form' === $context ) {
    $allowed_html = array(
      'strong' => array(),
      'em'     => array(),
      'br'     => array() // 允许换行
    );
  }
  return $allowed_html;
}
add_filter( 'wp_kses_allowed_html', 'my_kses_allowed_html', 10, 2 );

// 使用方法
$user_input = $_POST['my_custom_field']; // 假设这是用户输入
$safe_html = wp_kses( $user_input, wp_kses_allowed_html( array(), 'my_custom_form' ) );

echo $safe_html;

在这个例子中:

  1. 我们定义了一个名为 my_kses_allowed_html 的函数,该函数接收 $allowed_html$context 两个参数。
  2. $context 参数允许我们根据不同的场景使用不同的白名单。 这里我们定义了一个名为 'my_custom_form' 的上下文。
  3. 如果 $context'my_custom_form',我们就将 $allowed_html 数组设置为只允许使用 <strong>, <em><br> 标签。
  4. 我们使用 add_filter() 函数将 my_kses_allowed_html 函数添加到 wp_kses_allowed_html 过滤器中。
  5. 在使用 wp_kses() 函数时,我们将 $context 参数设置为 'my_custom_form',从而使用我们自定义的白名单。

更复杂的示例:允许带样式的段落

如果我们想允许用户使用 <p> 标签,并且允许用户使用 style 属性来设置段落的样式,但是只允许设置 colortext-align 两个样式属性,该怎么做呢?

这有点复杂,因为 wp_kses() 默认情况下不会对 style 属性的值进行过滤。 为了实现这个目标,我们需要编写一个自定义的过滤函数,并将其添加到 wp_kses_attr 过滤器中。

function my_kses_style_attributes( $out, $tagname, $stylename, $stylevalue, $okprotocols ) {
    $allowed_styles = array(
        'color',
        'text-align',
    );

    if ( 'style' === $stylename && in_array( $tagname, array( 'p' ), true ) ) {
        if ( in_array( strtolower( trim( $stylevalue ) ), $allowed_styles, true ) ) {
            return $out; // 允许该样式
        } else {
            return ''; // 拒绝该样式
        }
    }

    return $out; // 其他情况保持不变
}

add_filter( 'wp_kses_attr', 'my_kses_style_attributes', 10, 5 );

function my_kses_allowed_html_with_style( $allowed_html ) {
    $allowed_html['p'] = array(
        'style' => true,
    );
    return $allowed_html;
}

add_filter( 'wp_kses_allowed_html', 'my_kses_allowed_html_with_style' );

// 使用方法
$user_input = '<p style="color: red; text-align: center; font-size: 20px;">这是一个带样式的段落。</p>';
$safe_html = wp_kses( $user_input, wp_kses_allowed_html() );

echo $safe_html; // 输出: <p style="color: red; text-align: center;">这是一个带样式的段落。</p>

在这个例子中:

  1. my_kses_style_attributes 函数用于过滤 style 属性的值。 它首先检查标签名是否为 'p',然后检查 style 属性的值是否为 colortext-align。 如果是,就允许该样式;否则,就拒绝该样式。
  2. my_kses_allowed_html_with_style 函数用于允许 <p> 标签的 style 属性。
  3. 我们分别使用 add_filter() 函数将 my_kses_style_attributesmy_kses_allowed_html_with_style 函数添加到 wp_kses_attrwp_kses_allowed_html 过滤器中。

总结

wp_kses() 是WordPress安全的重要组成部分。 理解它的工作原理,并学会自定义白名单,可以帮助你更好地保护你的网站免受恶意代码的攻击。

当然,wp_kses() 只是安全的第一步。 还有很多其他的安全措施需要采取,例如使用强密码、及时更新WordPress版本、安装安全插件等等。

希望今天的讲座对你有所帮助! 如果你有什么问题,欢迎在评论区留言。 咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注