探讨 `wp_kses()` 函数的源码,它是如何通过白名单机制过滤 HTML 的?

嘿,各位代码界的冒险家们,准备好踏上一次 HTML 过滤的奇妙旅程了吗?今天咱们要聊聊 WordPress 里那个大名鼎鼎的 wp_kses() 函数,看看它如何像一位一丝不苟的安检员,用白名单机制把不怀好意的 HTML 标签和属性挡在门外。

开场白:HTML 的狂野西部

想象一下,如果没有 wp_kses(),WordPress 就好比一个没有保安的狂野西部小镇,任何 HTML 代码都可以随便进出。这意味着恶意用户可以轻松地插入脚本、iframe,甚至更糟糕的东西,把你的网站变成他们的游乐场。

wp_kses() 的出现,就是为了给这个混乱的局面带来秩序。它通过一套预定义的规则(也就是白名单),决定哪些 HTML 标签和属性是安全的,可以被允许通过,而其他的则会被无情地剥夺。

wp_kses() 的核心:白名单

白名单是 wp_kses() 的灵魂。它定义了哪些 HTML 标签和属性是被允许的。你可以把它想象成一个俱乐部的会员名单,只有名单上的人才能进入。

wp_kses() 默认使用一个内置的白名单,但你也可以根据自己的需要进行修改。这就像你可以决定谁可以加入你的俱乐部一样。

wp_kses() 的基本用法

先来看看 wp_kses() 的基本用法:

<?php
$allowed_html = array(
    'p' => array(
        'class' => array(),
    ),
    'a' => array(
        'href' => array(),
        'title' => array(),
    ),
    'br' => array(),
    'em' => array(),
    'strong' => array(),
);

$unsafe_html = '<p class="lead">这是一个段落,包含一个 <a href="javascript:alert('XSS')">链接</a> 和一些 <em>强调</em> <strong>加粗</strong> 的文字。</p><script>alert("XSS");</script>';

$safe_html = wp_kses( $unsafe_html, $allowed_html );

echo "原始 HTML:n" . $unsafe_html . "nn";
echo "过滤后的 HTML:n" . $safe_html . "n";
?>

在这个例子中:

  • $allowed_html 定义了一个白名单,允许 p (带 class 属性), a (带 hreftitle 属性), br, em, 和 strong 标签。
  • $unsafe_html 包含一些 HTML 代码,包括一个 XSS 漏洞 (JavaScript 链接) 和一个 <script> 标签。
  • wp_kses() 函数接受两个参数:要过滤的 HTML 字符串和白名单。
  • $safe_html 存储了过滤后的 HTML,它移除了不安全的 script 标签和 a 标签中的 javascript: URL。

剖析 wp_kses() 的源码

现在,让我们深入 wp_kses() 的源码,看看它是如何工作的。由于 wp_kses() 本身是一个比较复杂的函数,我们将重点关注其核心逻辑和数据结构。

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

  1. 准备工作:

    • 初始化一些变量,比如 HTML 标签和属性的白名单。
    • 对输入的 HTML 进行一些预处理,比如将 HTML 实体转换为字符。
  2. 词法分析(Tokenization):

    • 将 HTML 字符串分解成一个个的 "token",比如开始标签、结束标签、文本内容等等。 这部分逻辑通常比较复杂,需要处理各种 HTML 语法细节。
    • kses_tokenizer() 函数负责这项任务。
  3. 过滤:

    • 遍历 token 列表,根据白名单决定哪些 token 可以保留,哪些需要移除。
    • wp_kses_bad_protocol() 函数用于检查 URL 的协议是否安全(例如,不允许 javascript:)。
    • wp_kses_attr() 函数用于过滤 HTML 属性。
  4. 组装:

    • 将过滤后的 token 重新组装成 HTML 字符串。

核心函数和数据结构

  • $allowed_html (白名单数组): 这是一个多维数组,定义了允许的 HTML 标签和属性。数组的键是标签名,值是一个数组,包含允许的属性。例如:

    $allowed_html = array(
        'p' => array(
            'class' => array(),
            'style' => array(), //允许 style 属性
        ),
        'a' => array(
            'href' => array(),
            'title' => array(),
            'rel' => array(),
        ),
        'img' => array(
            'src' => array(),
            'alt' => array(),
            'width' => array(),
            'height' => array(),
        ),
    );

    在上面的例子中,我们允许 p 标签拥有 classstyle 属性,a 标签拥有 hreftitlerel 属性,img 标签拥有 srcaltwidthheight 属性。

  • kses_tokenizer() 这个函数负责将 HTML 字符串分解成 token。它使用正则表达式来匹配 HTML 标签、属性和文本内容。 由于正则表达式比较复杂,这里就不深入讲解了。

  • wp_kses_bad_protocol() 这个函数用于检查 URL 的协议是否安全。它会检查 URL 是否以 http:, https:, ftp:, mailto:, tel:, 或 data: 开头。如果 URL 使用了其他协议(比如 javascript:),则会被移除。

  • wp_kses_attr() 这个函数用于过滤 HTML 属性。它会检查属性是否在白名单中,并且属性值是否安全。 它会调用 wp_kses_bad_protocol() 来检查 URL 属性的协议。

wp_kses_attr() 源码分析

我们来稍微深入地看一下 wp_kses_attr() 函数,因为它在属性过滤中起着至关重要的作用。 简化后的 wp_kses_attr() 大致如下:

<?php
function wp_kses_attr( $element, $attribute, $value, $allowed_html, $prot = 'html' ) {
    $value = trim( $value );

    if ( empty( $allowed_html[ $element ] ) || ! is_array( $allowed_html[ $element ] ) ) {
        return false; // 该标签不允许任何属性
    }

    if ( ! isset( $allowed_html[ $element ][ $attribute ] ) ) {
        return false; // 该属性不允许
    }

    if ( 'href' === $attribute || 'src' === $attribute ) {
        $value = wp_kses_bad_protocol( $value, $allowed_html, $prot );
        if ( empty( $value ) ) {
            return false; // 不安全的协议,移除属性
        }
    }

    // 其他安全检查...

    return $value; // 属性值安全,返回
}

// 示例用法
$allowed_html = array(
    'a' => array(
        'href' => array(),
        'title' => array(),
    ),
);

$element = 'a';
$attribute = 'href';
$value = 'javascript:void(0)'; // 不安全的 URL

$safe_value = wp_kses_attr( $element, $attribute, $value, $allowed_html );

if ( false === $safe_value ) {
    echo "属性被移除n";
} else {
    echo "安全属性值: " . $safe_value . "n";
}
?>

在这个简化版本中:

  1. 它首先检查白名单中是否允许该标签和属性。
  2. 如果属性是 hrefsrc,则调用 wp_kses_bad_protocol() 检查 URL 的协议。
  3. 如果协议不安全,则返回 false,表示该属性应该被移除。
  4. 最后,如果属性值安全,则返回该值。

自定义白名单

wp_kses() 的强大之处在于你可以自定义白名单。你可以根据自己的需要添加或删除标签和属性。

例如,如果你想允许 iframe 标签,你可以这样做:

<?php
$allowed_html = array(
    'iframe' => array(
        'src' => array(),
        'width' => array(),
        'height' => array(),
        'frameborder' => array(),
        'allowfullscreen' => array(),
    ),
);

$unsafe_html = '<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="560" height="315" frameborder="0" allowfullscreen></iframe>';

$safe_html = wp_kses( $unsafe_html, $allowed_html );

echo "过滤后的 HTML:n" . $safe_html . "n";
?>

在这个例子中,我们添加了 iframe 标签到白名单,并允许它拥有 srcwidthheightframeborderallowfullscreen 属性。

wp_kses_post() 函数

WordPress 还提供了一个名为 wp_kses_post() 的函数,它使用一个预定义的白名单,专门用于过滤文章内容。这个白名单包含了常用的 HTML 标签和属性,比如 paimgstrongem 等等。 wp_kses_post() 函数通常用于过滤用户提交的文章内容,以防止 XSS 攻击。

注意事项和最佳实践

  • 永远不要信任用户输入: 即使你使用了 wp_kses(),也不要完全信任用户输入。始终对用户输入进行验证和转义,以防止其他类型的安全漏洞。
  • 保持白名单尽可能小: 只允许你需要的标签和属性。白名单越大,安全风险就越高。
  • 定期审查白名单: 随着 HTML 标准的发展,新的标签和属性可能会出现。定期审查你的白名单,确保它仍然是最新的和安全的。
  • 测试你的过滤规则: 在生产环境中使用 wp_kses() 之前,务必测试你的过滤规则,以确保它们能够正确地过滤不安全的 HTML 代码,同时又不会误伤正常的 HTML 代码。
  • 了解上下文: 不同的上下文可能需要不同的过滤规则。例如,在文章内容中允许的标签和属性,可能在评论中就不允许。
  • 转义HTML实体: 使用 esc_html()esc_attr() 函数来转义HTML实体,这可以防止某些类型的XSS攻击。

高级技巧:使用 wp_kses_allowed_html 过滤器

WordPress 提供了 wp_kses_allowed_html 过滤器,允许你全局修改 wp_kses() 使用的白名单。这对于插件和主题开发者来说非常有用,可以方便地添加或删除标签和属性。

<?php
add_filter( 'wp_kses_allowed_html', 'my_custom_kses_allowed_html', 10, 2 );

function my_custom_kses_allowed_html( $allowed_html, $context ) {
    if ( 'post' === $context ) {
        $allowed_html['span'] = array(
            'class' => array(),
            'style' => array(),
        );
    }

    return $allowed_html;
}
?>

在这个例子中,我们使用 wp_kses_allowed_html 过滤器,在 post 上下文中,允许 span 标签拥有 classstyle 属性。

总结

wp_kses() 是 WordPress 中一个非常重要的安全函数,它通过白名单机制过滤 HTML 代码,防止 XSS 攻击。理解 wp_kses() 的工作原理,可以帮助你编写更安全的代码,保护你的网站免受恶意攻击。记住,安全是一个持续的过程,需要不断地学习和实践。

希望这次的 HTML 过滤冒险之旅对你有所帮助!记住,代码世界充满了挑战和乐趣,保持好奇心,不断探索,你一定能成为一名出色的代码冒险家!

发表回复

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