深入理解 WordPress `wp_kses_post()` 函数的源码:如何过滤文章内容中的 HTML 以防止 XSS。

各位观众老爷,大家好!欢迎来到今天的“WordPress 源码探秘”系列讲座。今天咱们要扒的是 WordPress 里一个非常重要的函数——wp_kses_post()。这玩意儿就像个尽职尽责的门卫,专门负责过滤文章内容里的 HTML,防止那些不怀好意的 XSS 攻击溜进来。

先别被 XSS 吓着,简单来说,XSS (Cross-Site Scripting) 就是攻击者偷偷往你的网站里塞一段 JavaScript 代码,用户一访问你的网站,这段恶意代码就跑起来了,轻则篡改页面,重则盗取用户 Cookie,那可就麻烦大了。

wp_kses_post() 就像一道防火墙,它会把文章内容里的 HTML 标签和属性进行严格检查,只允许那些安全的、对用户友好的标签和属性通过。

一、wp_kses_post() 的身世背景

wp_kses_post() 其实是 wp_kses() 函数的一个特例。 wp_kses() 才是真正的过滤大杀器,它能根据你指定的规则(允许哪些标签,允许哪些属性)来过滤 HTML。而 wp_kses_post() 呢,就是预设了一套适合文章内容的规则,简化了我们的操作。

二、wp_kses_post() 的用法

用法很简单,直接把你要过滤的内容丢给它就行了:

$unsafe_html = '<script>alert("XSS!")</script><p style="color:red;">Hello, world!</p><a href="javascript:void(0);">Click me</a>';
$safe_html = wp_kses_post( $unsafe_html );
echo $safe_html; // 输出:<p style="color:red;">Hello, world!</p><a>Click me</a>

可以看到,讨厌的 <script> 标签被无情地干掉了,javascript: 链接也被处理掉了。

三、 深入 wp_kses_post() 的源码

好了,接下来是重头戏,咱们一起扒一扒 wp_kses_post() 的源码,看看它到底是怎么工作的。

function wp_kses_post( $data ) {
    $allowed_tags = wp_kses_allowed_html( 'post' );
    return wp_kses( $data, $allowed_tags );
}

是不是很简单? 只有两行代码!

  • 第一行:$allowed_tags = wp_kses_allowed_html( 'post' ); 这行代码是关键,它调用了 wp_kses_allowed_html() 函数,并传入了 'post' 参数。这个参数告诉 wp_kses_allowed_html() 函数,我们要获取的是适用于文章内容的 HTML 标签和属性的白名单。
  • 第二行:return wp_kses( $data, $allowed_tags ); 这行代码调用了 wp_kses() 函数,把要过滤的 HTML 数据 $data 和白名单 $allowed_tags 传给它,让它按照白名单的规则进行过滤,最后返回过滤后的安全 HTML。

所以,wp_kses_post() 本身只是一个包装器,它真正的工作都交给了 wp_kses_allowed_html()wp_kses() 这两个函数。

四、wp_kses_allowed_html():白名单的制造者

wp_kses_allowed_html() 函数负责生成 HTML 标签和属性的白名单。 让我们看看它的源码:

function wp_kses_allowed_html( $context = 'post' ) {
    global $allowedposttags, $allowedtags;

    if ( 'post' === $context ) {
        if ( empty( $allowedposttags ) ) {
            $allowedposttags = apply_filters( 'wp_kses_allowed_html', _wp_kses_allowed_html( 'post' ), 'post' );
        }
        return $allowedposttags;
    } elseif ( 'strip' === $context ) {
        if ( empty( $allowedtags ) ) {
            $allowedtags = apply_filters( 'wp_kses_allowed_html', _wp_kses_allowed_html( 'strip' ), 'strip' );
        }
        return $allowedtags;
    } else {
        return apply_filters( 'wp_kses_allowed_html', _wp_kses_allowed_html( $context ), $context );
    }
}

这个函数看起来稍微复杂一些,但核心思想很简单:

  1. 根据上下文获取白名单: 函数首先根据传入的 $context 参数来决定要获取哪个白名单。 如果 $context'post',表示获取文章内容的白名单;如果是 'strip',表示获取用于剥离 HTML 标签的白名单;如果是其他值,则获取对应的白名单。
  2. 使用全局变量缓存白名单: 为了提高性能,函数使用了全局变量 $allowedposttags$allowedtags 来缓存白名单。 如果白名单已经存在,则直接返回缓存的白名单;否则,调用 _wp_kses_allowed_html() 函数生成白名单,并将其缓存到全局变量中。
  3. 使用过滤器 wp_kses_allowed_html 允许自定义白名单: 函数使用了 apply_filters() 函数,允许开发者通过 wp_kses_allowed_html 过滤器来修改白名单。 这意味着你可以根据自己的需要,添加或删除允许的 HTML 标签和属性。

五、_wp_kses_allowed_html():白名单的定义者

_wp_kses_allowed_html() 函数才是真正定义白名单的地方。 让我们看看它的源码:

function _wp_kses_allowed_html( $context ) {
    $allowed_html = array();

    switch ( $context ) {
        case 'post':
            $allowed_html = array(
                'address'    => array(),
                'a'          => array(
                    'href'   => true,
                    'title'  => true,
                    'target' => true,
                    'rel'    => true,
                ),
                'abbr'       => array(
                    'title' => true,
                ),
                'acronym'    => array(
                    'title' => true,
                ),
                'area'       => array(
                    'alt'    => true,
                    'coords' => true,
                    'href'   => true,
                    'shape'  => true,
                    'target' => true,
                ),
                'b'          => array(),
                'big'        => array(),
                'blockquote' => array(
                    'cite' => true,
                ),
                'br'         => array(),
                'button'     => array(
                    'disabled' => true,
                    'name'     => true,
                    'type'     => true,
                    'value'    => true,
                ),
                'caption'    => array(
                    'align' => true,
                    'class' => true,
                ),
                'cite'       => array(),
                'code'       => array(),
                'col'        => array(
                    'align'   => true,
                    'span'    => true,
                    'width'   => true,
                ),
                'colgroup'   => array(
                    'align'   => true,
                    'span'    => true,
                    'width'   => true,
                ),
                'del'        => array(
                    'datetime' => true,
                    'cite'     => true,
                ),
                'dd'         => array(),
                'dfn'        => array(
                    'title' => true,
                ),
                'div'        => array(
                    'align' => true,
                    'class' => true,
                    'dir'   => true,
                    'id'    => true,
                    'lang'  => true,
                    'style' => true,
                    'xml:lang' => true,
                ),
                'dl'         => array(),
                'dt'         => array(),
                'em'         => array(),
                'fieldset'   => array(),
                'font'       => array(
                    'color' => true,
                    'face'  => true,
                    'size'  => true,
                ),
                'form'       => array(
                    'action'  => true,
                    'accept'  => true,
                    'accept-charset' => true,
                    'enctype' => true,
                    'method'  => true,
                    'name'    => true,
                    'target'  => true,
                ),
                'h1'         => array(
                    'align' => true,
                    'class' => true,
                    'id'    => true,
                ),
                'h2'         => array(
                    'align' => true,
                    'class' => true,
                    'id'    => true,
                ),
                'h3'         => array(
                    'align' => true,
                    'class' => true,
                    'id'    => true,
                ),
                'h4'         => array(
                    'align' => true,
                    'class' => true,
                    'id'    => true,
                ),
                'h5'         => array(
                    'align' => true,
                    'class' => true,
                    'id'    => true,
                ),
                'h6'         => array(
                    'align' => true,
                    'class' => true,
                    'id'    => true,
                ),
                'hr'         => array(
                    'align'   => true,
                    'class'   => true,
                    'noshade' => true,
                    'size'    => true,
                    'width'   => true,
                ),
                'i'          => array(),
                'img'        => array(
                    'alt'      => true,
                    'align'    => true,
                    'border'   => true,
                    'height'   => true,
                    'hspace'   => true,
                    'longdesc' => true,
                    'vspace'   => true,
                    'src'      => true,
                    'width'    => true,
                ),
                'input'      => array(
                    'align'    => true,
                    'alt'      => true,
                    'checked'  => true,
                    'disabled' => true,
                    'maxlength' => true,
                    'name'     => true,
                    'readonly' => true,
                    'size'     => true,
                    'src'      => true,
                    'type'     => true,
                    'value'    => true,
                ),
                'ins'        => array(
                    'datetime' => true,
                    'cite'     => true,
                ),
                'kbd'        => array(),
                'label'      => array(
                    'for' => true,
                ),
                'legend'     => array(
                    'align' => true,
                ),
                'li'         => array(
                    'align' => true,
                    'class' => true,
                    'dir'   => true,
                    'id'    => true,
                    'lang'  => true,
                    'style' => true,
                    'xml:lang' => true,
                ),
                'map'        => array(
                    'id'    => true,
                    'name'  => true,
                ),
                'mark'       => array(),
                'menu'       => array(),
                'meter'      => array(
                    'value' => true,
                    'min'   => true,
                    'max'   => true,
                ),
                'ol'         => array(
                    'align' => true,
                    'class' => true,
                    'dir'   => true,
                    'id'    => true,
                    'lang'  => true,
                    'start' => true,
                    'style' => true,
                    'type'  => true,
                    'xml:lang' => true,
                ),
                'optgroup'   => array(
                    'disabled' => true,
                    'label'    => true,
                ),
                'option'     => array(
                    'disabled' => true,
                    'selected' => true,
                    'value'    => true,
                ),
                'p'          => array(
                    'align' => true,
                    'class' => true,
                    'dir'   => true,
                    'id'    => true,
                    'lang'  => true,
                    'style' => true,
                    'xml:lang' => true,
                ),
                'pre'        => array(
                    'align' => true,
                    'class' => true,
                    'width' => true,
                ),
                'progress'   => array(
                    'value' => true,
                    'max'   => true,
                ),
                'q'          => array(
                    'cite' => true,
                ),
                's'          => array(),
                'samp'       => array(),
                'select'     => array(
                    'disabled' => true,
                    'form'     => true,
                    'multiple' => true,
                    'name'     => true,
                    'size'     => true,
                ),
                'small'      => array(),
                'span'       => array(
                    'align' => true,
                    'class' => true,
                    'dir'   => true,
                    'id'    => true,
                    'lang'  => true,
                    'style' => true,
                    'title' => true,
                    'xml:lang' => true,
                ),
                'strong'     => array(),
                'sub'        => array(),
                'summary'    => array(),
                'sup'        => array(),
                'table'      => array(
                    'align'       => true,
                    'bgcolor'     => true,
                    'border'      => true,
                    'cellpadding' => true,
                    'cellspacing' => true,
                    'class' => true,
                    'dir'   => true,
                    'id'    => true,
                    'lang'  => true,
                    'style' => true,
                    'summary'     => true,
                    'width'       => true,
                    'xml:lang' => true,
                ),
                'tbody'      => array(
                    'align'   => true,
                    'char'    => true,
                    'charoff' => true,
                    'valign'  => true,
                ),
                'td'         => array(
                    'abbr'    => true,
                    'align'   => true,
                    'axis'    => true,
                    'bgcolor' => true,
                    'char'    => true,
                    'charoff' => true,
                    'class' => true,
                    'colspan' => true,
                    'dir'   => true,
                    'headers' => true,
                    'height'  => true,
                    'id'    => true,
                    'lang'  => true,
                    'rowspan' => true,
                    'scope'   => true,
                    'style' => true,
                    'valign'  => true,
                    'width'   => true,
                    'xml:lang' => true,
                ),
                'textarea'   => array(
                    'cols'        => true,
                    'rows'        => true,
                    'disabled'    => true,
                    'form'        => true,
                    'name'        => true,
                    'readonly'    => true,
                ),
                'tfoot'      => array(
                    'align'   => true,
                    'char'    => true,
                    'charoff' => true,
                    'valign'  => true,
                ),
                'th'         => array(
                    'abbr'    => true,
                    'align'   => true,
                    'axis'    => true,
                    'bgcolor' => true,
                    'char'    => true,
                    'charoff' => true,
                    'class' => true,
                    'colspan' => true,
                    'dir'   => true,
                    'headers' => true,
                    'height'  => true,
                    'id'    => true,
                    'lang'  => true,
                    'rowspan' => true,
                    'scope'   => true,
                    'style' => true,
                    'valign'  => true,
                    'width'   => true,
                    'xml:lang' => true,
                ),
                'thead'      => array(
                    'align'   => true,
                    'char'    => true,
                    'charoff' => true,
                    'valign'  => true,
                ),
                'time'       => array(
                    'datetime' => true,
                ),
                'title'      => array(),
                'tr'         => array(
                    'align'   => true,
                    'bgcolor' => true,
                    'char'    => true,
                    'charoff' => true,
                    'class' => true,
                    'dir'   => true,
                    'id'    => true,
                    'lang'  => true,
                    'style' => true,
                    'valign'  => true,
                    'xml:lang' => true,
                ),
                'tt'         => array(),
                'u'          => array(),
                'ul'         => array(
                    'align' => true,
                    'class' => true,
                    'dir'   => true,
                    'id'    => true,
                    'lang'  => true,
                    'style' => true,
                    'type'  => true,
                    'xml:lang' => true,
                ),
                'var'        => array(),
            );
            break;
        case 'strip':
            $allowed_html = array();
            break;
        default:
            $allowed_html = array();
            break;
    }

    return $allowed_html;
}

哇!一大段代码! 别怕,其实它就是定义了一个数组 $allowed_html,这个数组的键是允许的 HTML 标签,值是一个数组,表示该标签允许的属性。

例如:

'a'          => array(
    'href'   => true,
    'title'  => true,
    'target' => true,
    'rel'    => true,
),

这表示 <a> 标签是允许的,并且允许 hreftitletargetrel 这四个属性。 如果某个属性的值是 true,表示允许该属性;如果是 false 或者没有定义,表示不允许该属性。

你可以看到,_wp_kses_allowed_html() 函数针对不同的 $context 定义了不同的白名单。 'post' 上下文的白名单包含了大量的 HTML 标签和属性,而 'strip' 上下文的白名单则为空,表示不允许任何 HTML 标签。

六、wp_kses():HTML 过滤的核心引擎

wp_kses() 函数是 HTML 过滤的核心引擎。 它接收要过滤的 HTML 数据和白名单,然后根据白名单的规则进行过滤。 由于 wp_kses() 源码非常复杂,我们这里就不深入分析了,只简单介绍一下它的工作原理:

  1. 解析 HTML: wp_kses() 函数首先会把 HTML 数据解析成一个 DOM 树。
  2. 遍历 DOM 树: 然后,它会遍历 DOM 树的每一个节点(标签和属性)。
  3. 检查标签和属性: 对于每一个节点,它会检查该标签和属性是否在白名单中。 如果在,则保留该节点;否则,删除该节点。
  4. 重建 HTML: 最后,它会根据过滤后的 DOM 树重建 HTML 数据。

在检查属性时,wp_kses() 还会进行一些额外的安全检查,例如:

  • 过滤 javascript: 链接: wp_kses() 会把 href 属性中包含 javascript: 的链接替换成 #
  • 过滤 CSS 表达式: wp_kses() 会把 style 属性中包含 CSS 表达式的内容删除。
  • 过滤恶意 HTML 实体: wp_kses() 会把一些可能导致 XSS 攻击的 HTML 实体进行转义。

七、如何自定义 wp_kses_post() 的行为

WordPress 提供了多种方式来自定义 wp_kses_post() 的行为:

  1. 使用 wp_kses_allowed_html 过滤器: 这是最常用的方式。 你可以通过 wp_kses_allowed_html 过滤器来修改 wp_kses_post() 使用的白名单。

    add_filter( 'wp_kses_allowed_html', 'my_custom_kses_allowed_html', 10, 2 );
    
    function my_custom_kses_allowed_html( $allowed_tags, $context ) {
        if ( 'post' === $context ) {
            // 允许 <iframe> 标签,并允许 src、width 和 height 属性
            $allowed_tags['iframe'] = array(
                'src'    => true,
                'width'  => true,
                'height' => true,
                'frameborder' => true,
                'allowfullscreen' => true,
            );
            // 移除 <h1> 标签
            unset( $allowed_tags['h1'] );
        }
        return $allowed_tags;
    }

    这段代码会在 wp_kses_post() 的白名单中添加 <iframe> 标签,并允许 srcwidthheightframeborderallowfullscreen 属性,同时移除了 <h1> 标签。

  2. 使用 kses_allowed_protocols 过滤器: 这个过滤器允许你修改允许的 URL 协议。 默认情况下,wp_kses() 允许 httphttpsftpmailtonews 协议。

    add_filter( 'kses_allowed_protocols', 'my_custom_kses_allowed_protocols' );
    
    function my_custom_kses_allowed_protocols( $protocols ) {
        // 允许 `tel` 协议
        $protocols[] = 'tel';
        return $protocols;
    }

    这段代码会在允许的 URL 协议中添加 tel 协议。

  3. 自定义 wp_kses() 函数: 如果你需要更高级的自定义,你可以直接修改 wp_kses() 函数。 但是,这种方式不推荐,因为它会影响 WordPress 的核心代码,可能会导致兼容性问题。

八、总结

wp_kses_post() 函数是 WordPress 防止 XSS 攻击的重要工具。 它通过使用白名单来过滤 HTML 标签和属性,只允许那些安全的、对用户友好的标签和属性通过。 你可以通过 wp_kses_allowed_html 过滤器和 kses_allowed_protocols 过滤器来自定义 wp_kses_post() 的行为。

总的来说,理解 wp_kses_post() 函数的源码,能帮助我们更好地理解 WordPress 的安全机制,也能帮助我们更好地保护我们的网站免受 XSS 攻击。

希望今天的讲座对大家有所帮助! 如果大家有什么问题,欢迎提问。 下次再见!

总结表格:

函数/过滤器 描述 作用 如何自定义
wp_kses_post() 过滤文章内容的 HTML,防止 XSS 攻击的包装器函数。 简化 HTML 过滤操作,使用预定义的白名单。 使用 wp_kses_allowed_html 过滤器修改白名单。
wp_kses_allowed_html() 生成 HTML 标签和属性的白名单。 定义允许的 HTML 标签和属性。 使用 wp_kses_allowed_html 过滤器修改白名单。
_wp_kses_allowed_html() 真正定义 HTML 标签和属性白名单的函数。 存储默认允许的 HTML 标签和属性。 不直接修改,而是通过 wp_kses_allowed_html 过滤器间接修改。
wp_kses() HTML 过滤的核心引擎。 根据白名单过滤 HTML 数据,进行安全检查。 不推荐直接修改,影响核心代码,使用 wp_kses_allowed_htmlkses_allowed_protocols 过滤器。
kses_allowed_protocols 过滤器,允许修改允许的 URL 协议。 定义允许的 URL 协议,防止恶意链接。 使用 kses_allowed_protocols 过滤器添加或删除允许的协议。

XSS 防御最佳实践:

除了使用 wp_kses_post(),还有其他的 XSS 防御措施:

  • 输入验证: 对用户输入的数据进行验证,确保数据的格式和内容符合预期。
  • 输出编码: 在将数据输出到页面之前,对数据进行编码,防止恶意代码被执行。
  • 使用内容安全策略 (CSP): CSP 是一种安全策略,可以限制浏览器加载哪些资源,从而防止 XSS 攻击。
  • 保持 WordPress 和插件更新: 及时更新 WordPress 和插件,修复已知的安全漏洞。
  • 使用安全插件: 安装安全插件,可以提供额外的安全保护。

记住,安全无小事! 做好这些措施,才能有效地保护你的网站和用户免受 XSS 攻击的威胁。

发表回复

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