剖析 WordPress `wp_kses()` 函数的源码:如何通过白名单机制过滤 HTML 以防止 XSS 攻击。

各位观众老爷们,晚上好! 今天咱们不开车,来聊聊WordPress安全里一个重要的角色——wp_kses()。 这家伙的名字听起来像个秘密特工,实际上,它就是WordPress的HTML净化大师,专门负责把那些可能搞破坏的HTML标签和属性给咔嚓掉,防止XSS攻击。

XSS:Web安全界的“小强”

XSS (Cross-Site Scripting,跨站脚本攻击) 绝对是Web安全领域里生命力最顽强的“小强”之一。 简单来说,就是攻击者往你的网站里塞一些恶意的JavaScript代码,当用户浏览你的网站时,这些代码就会在用户的浏览器里执行,从而窃取用户的信息、篡改页面内容,甚至冒充用户身份搞事情。

想象一下,你辛辛苦苦搭建的博客,结果被别人插了广告,或者更过分,直接跳转到钓鱼网站,是不是想提刀砍人? 所以,防范XSS攻击至关重要。

wp_kses():白名单才是王道

wp_kses()函数的核心思想是“白名单”,而不是“黑名单”。 啥意思呢? 就是说,它不会去尝试识别所有可能的恶意标签和属性(这几乎是不可能的,因为攻击手段层出不穷),而是维护一个允许使用的标签和属性的列表,只有在这个列表里的HTML才能幸存下来,其他的统统干掉。

这种方式的好处是,即使出现新的攻击手段,只要不在白名单里,就无法生效。 相比之下,黑名单的方式很容易被绕过,因为总有你没想到的攻击方式。

wp_kses() 的基本用法

wp_kses() 函数的基本用法如下:

$allowed_html = array(
    'a' => array(
        'href' => array(),
        'title' => array(),
    ),
    'br' => array(),
    'em' => array(),
    'strong' => array(),
);

$string = '<a href="https://example.com" title="Example">Example Link</a><br><em>Emphasis</em> <strong>Strong</strong> <script>alert("XSS");</script>';

$ksesed_string = wp_kses( $string, $allowed_html );

echo $ksesed_string; // 输出:<a href="https://example.com" title="Example">Example Link</a><br><em>Emphasis</em> <strong>Strong</strong>

在这个例子中,我们定义了一个 $allowed_html 数组,它指定了允许使用的HTML标签和属性。 然后,我们把一个包含恶意脚本的字符串传递给 wp_kses() 函数,经过处理后,恶意脚本被移除了,只留下了白名单里的标签和属性。

深入 wp_kses() 的源码

要真正理解 wp_kses() 的强大之处,我们需要深入它的源码。 别怕,我会尽量用通俗易懂的方式来讲解。

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

  1. 预处理: 对输入字符串进行一些预处理,例如移除BOM头。
  2. HTML解析: 使用正则表达式将HTML字符串分解成标签、属性和文本节点。
  3. 标签过滤: 遍历所有标签,检查它们是否在白名单中。 如果不在,则移除该标签。
  4. 属性过滤: 对于白名单中的标签,遍历它们的属性,检查属性是否在白名单中。 如果不在,则移除该属性。 还会进行属性值的安全检查,例如检查URL是否合法。
  5. 后处理: 将过滤后的标签、属性和文本节点重新组合成HTML字符串。

核心函数:wp_kses_split()

wp_kses_split() 函数是HTML解析的关键,它使用正则表达式将HTML字符串分解成不同的部分。 它的核心代码如下:

function wp_kses_split( $string, $allowed_html, $allowed_protocols ) {
    $string = preg_replace( '/<!--.*?-->/s', '', $string ); // Remove comments
    $string = preg_replace( '/<!--(.*)-->/Uis', '', $string ); // Remove comments
    $string = preg_replace('/[rnt ]+/', ' ', $string);
    preg_match_all('%
        (
            <[^<>]+
            |
            [^<>]+
        )
        %xs', $string, $matches );

    $chunks = array();
    foreach ( $matches[1] as $i => $match ) {
        if ( '<' === $match[0] ) {
            if ( 0 === strpos( $match, '<!--' ) ) {
                continue;
            }

            $pieces = wp_kses_attr( $match, $allowed_html, $allowed_protocols );

            if ( empty( $pieces ) ) {
                continue;
            }

            $chunks[] = $pieces;
        } else {
            $chunks[] = wp_kses_no_null( wp_kses_html_error( $match ) );
        }
    }

    return implode( '', $chunks );
}

这个函数使用正则表达式 /</?w.*?>/ 来匹配HTML标签。 然后,它会调用 wp_kses_attr() 函数来处理标签的属性。

核心函数:wp_kses_attr()

wp_kses_attr() 函数负责过滤标签的属性。 它的核心代码如下:

function wp_kses_attr( $element, $allowed_html, $allowed_protocols ) {

    $htmlregex = ''; // Build regex based on $allowed_html
    $allowed_atts = array();
    foreach ( (array) $allowed_html as $tag => $atts ) {
        if ( ! isset( $atts['*'] ) ) {
            continue;
        }

        $htmlregex .= '<' . preg_quote( $tag ) . '[^>]*>|';

        foreach ( (array) $atts as $att => $val ) {
            $allowed_atts[ $tag ][ $att ] = true;
        }
    }

    $htmlregex = substr( $htmlregex, 0, -1 ); // Cut off the last '|'
    $htmlregex = '/(' . $htmlregex . ')/i';

    preg_match_all( '/(<[^<>]+>)/i', $element, $matches );
    if ( empty( $matches[1] ) ) {
        return '';
    }

    $element = $matches[1][0];
    preg_match_all( '%(?s) ([a-zA-Z_:]+)=(['"])([^2]*?)2%', $element, $atts );
    preg_match_all( '%(?s) ([a-zA-Z_:]+)=([^ '"]*)%', $element, $u_atts );

    $attributes = array();
    if ( ! empty( $atts[1] ) ) {
        foreach ( (array) $atts[1] as $i => $name ) {
            $attributes[ strtolower( $name ) ] = $atts[3][ $i ];
        }
    }

    if ( ! empty( $u_atts[1] ) ) {
        foreach ( (array) $u_atts[1] as $i => $name ) {
            $attributes[ strtolower( $name ) ] = $u_atts[2][ $i ];
        }
    }

    if ( empty( $attributes ) ) {
        return $element;
    }

    $output = '<' . substr( $element, 1, strpos( $element, ' ' ) - 1 );

    foreach ( (array) $attributes as $name => $value ) {
        if ( ! isset( $allowed_atts[ strtolower( substr( $element, 1, strpos( $element, ' ' ) - 1 ) ) ][ $name ] ) ) {
            continue;
        }

        $value = wp_kses_bad_protocol( $value, $allowed_protocols );
        $value = esc_attr( $value );

        $output .= ' ' . $name . '="' . $value . '"';
    }

    $output .= '>';

    return $output;
}

这个函数首先提取出标签的所有属性,然后遍历这些属性,检查它们是否在白名单中。 如果属性在白名单中,它还会调用 wp_kses_bad_protocol() 函数来检查属性值是否包含恶意协议,例如 javascript:

核心函数:wp_kses_bad_protocol()

wp_kses_bad_protocol() 函数负责检查属性值是否包含恶意协议。 它的核心代码如下:

function wp_kses_bad_protocol( $string, $allowed_protocols ) {
    $string = wp_kses_no_null( $string );
    $string = strtolower( $string );

    // Unencoded entities.
    $string = wp_kses_decode_entities( $string );

    $string = preg_replace( '/s/', '', $string );

    do {
        $original_string = $string;
        foreach ( (array) $allowed_protocols as $protocol ) {
            $string = str_replace( $protocol . ':', '', $string );
        }
    } while ( $original_string !== $string );

    return $string;
}

这个函数首先移除字符串中的空格和NULL字符,然后将字符串转换为小写。 接着,它遍历所有允许的协议,例如 httphttps,将字符串中包含这些协议的部分移除。 如果移除后字符串仍然包含其他协议,则说明存在恶意协议。

自定义白名单:让 wp_kses() 更灵活

虽然 wp_kses() 已经提供了一个默认的白名单,但在实际应用中,我们可能需要自定义白名单,以满足特定的需求。 例如,我们可能需要允许使用 <iframe> 标签来嵌入视频,或者允许使用 data- 属性来存储自定义数据。

要自定义白名单,我们可以使用 wp_kses_allowed_html 过滤器。 例如,以下代码允许使用 <iframe> 标签,并允许使用 srcwidth 属性:

function my_kses_allow_iframe( $allowed_tags, $context ) {
    if ( $context === 'post' ) { // 只在文章内容中使用
        $allowed_tags['iframe'] = array(
            'src' => true,
            'width' => true,
            'height' => true,
            'frameborder' => true,
            'allowfullscreen' => true,
        );
    }
    return $allowed_tags;
}
add_filter( 'wp_kses_allowed_html', 'my_kses_allow_iframe', 10, 2 );

常见问题与注意事项

  • 过度过滤: 过度过滤可能会导致一些合法的HTML标签被移除,从而影响页面的显示效果。 因此,我们需要仔细权衡安全性和可用性,选择合适的白名单。
  • 编码问题: 在处理HTML字符串时,需要注意编码问题,例如确保字符串是UTF-8编码。
  • 上下文: 不同的上下文可能需要不同的白名单。 例如,文章内容和评论内容可能需要不同的过滤规则。
  • 更新: 随着Web安全技术的不断发展,攻击手段也在不断变化。 因此,我们需要定期更新WordPress,以获取最新的安全补丁和过滤规则。
  • 不要完全依赖wp_kses() wp_kses()是一个强大的工具,但它不是万能的。 在处理用户输入时,我们还需要采取其他安全措施,例如输入验证和输出编码。

wp_kses() 白名单示例

标签 属性 说明
a href, title, target (可能需要限制 target 属性的值,例如只允许 _blank) 链接标签
abbr title 缩写标签
acronym title 缩写标签 (已废弃,建议使用 <abbr>)
b (无属性) 粗体标签
blockquote cite 引用标签
br (无属性) 换行标签
code (无属性) 代码标签
em (无属性) 强调标签
i (无属性) 斜体标签
img src, alt, width, height, class, srcset, sizes (需要对 src 属性进行严格的URL验证) 图片标签
li class 列表项标签
ol class, start 有序列表标签
p class 段落标签
q cite 短引用标签
small (无属性) 小号字体标签
span class, title 行内容器标签
strong (无属性) 强调标签
sub (无属性) 下标标签
sup (无属性) 上标标签
ul class 无序列表标签

总结

wp_kses() 函数是WordPress安全体系中一个非常重要的组成部分。 通过使用白名单机制,它可以有效地防止XSS攻击,保护你的网站和用户的安全。 但是,我们需要理解 wp_kses() 的工作原理,并根据实际需求自定义白名单,才能充分发挥它的作用。 同时,我们也要意识到 wp_kses() 不是万能的,还需要结合其他安全措施,才能构建一个安全可靠的WordPress网站。

好了,今天的讲座就到这里。 感谢各位的观看,希望对大家有所帮助! 记住,安全无小事,防范XSS,人人有责!

发表回复

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