各位观众老爷,大家好!欢迎来到今天的“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 );
}
}
这个函数看起来稍微复杂一些,但核心思想很简单:
- 根据上下文获取白名单: 函数首先根据传入的
$context
参数来决定要获取哪个白名单。 如果$context
是'post'
,表示获取文章内容的白名单;如果是'strip'
,表示获取用于剥离 HTML 标签的白名单;如果是其他值,则获取对应的白名单。 - 使用全局变量缓存白名单: 为了提高性能,函数使用了全局变量
$allowedposttags
和$allowedtags
来缓存白名单。 如果白名单已经存在,则直接返回缓存的白名单;否则,调用_wp_kses_allowed_html()
函数生成白名单,并将其缓存到全局变量中。 - 使用过滤器
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>
标签是允许的,并且允许 href
、title
、target
和 rel
这四个属性。 如果某个属性的值是 true
,表示允许该属性;如果是 false
或者没有定义,表示不允许该属性。
你可以看到,_wp_kses_allowed_html()
函数针对不同的 $context
定义了不同的白名单。 'post'
上下文的白名单包含了大量的 HTML 标签和属性,而 'strip'
上下文的白名单则为空,表示不允许任何 HTML 标签。
六、wp_kses()
:HTML 过滤的核心引擎
wp_kses()
函数是 HTML 过滤的核心引擎。 它接收要过滤的 HTML 数据和白名单,然后根据白名单的规则进行过滤。 由于 wp_kses()
源码非常复杂,我们这里就不深入分析了,只简单介绍一下它的工作原理:
- 解析 HTML:
wp_kses()
函数首先会把 HTML 数据解析成一个 DOM 树。 - 遍历 DOM 树: 然后,它会遍历 DOM 树的每一个节点(标签和属性)。
- 检查标签和属性: 对于每一个节点,它会检查该标签和属性是否在白名单中。 如果在,则保留该节点;否则,删除该节点。
- 重建 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()
的行为:
-
使用
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>
标签,并允许src
、width
、height
、frameborder
和allowfullscreen
属性,同时移除了<h1>
标签。 -
使用
kses_allowed_protocols
过滤器: 这个过滤器允许你修改允许的 URL 协议。 默认情况下,wp_kses()
允许http
、https
、ftp
、mailto
和news
协议。add_filter( 'kses_allowed_protocols', 'my_custom_kses_allowed_protocols' ); function my_custom_kses_allowed_protocols( $protocols ) { // 允许 `tel` 协议 $protocols[] = 'tel'; return $protocols; }
这段代码会在允许的 URL 协议中添加
tel
协议。 -
自定义
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_html 和 kses_allowed_protocols 过滤器。 |
kses_allowed_protocols |
过滤器,允许修改允许的 URL 协议。 | 定义允许的 URL 协议,防止恶意链接。 | 使用 kses_allowed_protocols 过滤器添加或删除允许的协议。 |
XSS 防御最佳实践:
除了使用 wp_kses_post()
,还有其他的 XSS 防御措施:
- 输入验证: 对用户输入的数据进行验证,确保数据的格式和内容符合预期。
- 输出编码: 在将数据输出到页面之前,对数据进行编码,防止恶意代码被执行。
- 使用内容安全策略 (CSP): CSP 是一种安全策略,可以限制浏览器加载哪些资源,从而防止 XSS 攻击。
- 保持 WordPress 和插件更新: 及时更新 WordPress 和插件,修复已知的安全漏洞。
- 使用安全插件: 安装安全插件,可以提供额外的安全保护。
记住,安全无小事! 做好这些措施,才能有效地保护你的网站和用户免受 XSS 攻击的威胁。