搜索全站
插件 15 分钟阅读

WordPress 数据验证与转义函数对照表(sanitize / esc / wp_kses)

什么时候用 sanitize、什么时候用 esc?本文给出输入净化、输出转义、富文本过滤三类函数的完整对照表,附典型场景代码示例与最容易写错的六个地方。

U
UFO_PF 2017-09-08 · 阅读 15 分钟
Wo 插件
阅读提示
本文约 5,634 字,预计阅读 15 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:同样一段数据,两个地方要处理两次

「为什么要转义两次?」这是很多人第一次看到「入库前 sanitize_text_field(),出库时还要 esc_html()」时的疑问。

答案是这两个动作防的是不同的事,而且都不能省。

入库前净化(sanitize) 解决的是「数据该以什么形态被保存」。用户提交了一个标题,你不知道里面有没有多余的空白、有没有非法字符、有没有伪造的换行。净化是把数据规整成你期望的样子,让数据库里的内容是可预期的。

出库时转义(esc) 解决的是「这段内容在 HTML 里该怎么显示」。即使入库时净化过,输出时仍然必须转义——因为数据库中可能还有历史数据、其他插件写入的数据,而且 esc_html() 防的是浏览器把它当标签解析。

漏掉前者,数据库里会有脏数据;漏掉后者,就是存储型 XSS。安全扫描报告里最常见的插件漏洞类型,就是输出时漏转义。

本文把这三类函数整理成对照表,并给出可直接套用的写法。

原理分析:三类函数的分工

类别 代表函数 使用时机 防的是什么
输入净化 sanitize_* 数据写入数据库之前 脏数据、非法格式
输出转义 esc_* 数据输出到 HTML / 属性 / URL 之前 XSS、HTML 结构破坏
富文本过滤 wp_kses* 允许保留部分 HTML 标签时 恶意标签与属性
类型转换 absint / intval / floatval 数字类输入 类型混淆
反转义处理 wp_unslash 读取 $_POST / $_GET 之后 WordPress 自动添加的斜杠

关于 wp_unslash():WordPress 为了兼容老代码会对 $_POST、$_GET、$_COOKIE 做 addslashes() 处理(即 wp_magic_quotes())。所以从超全局变量读数据时的标准顺序是先 wp_unslash() 再 sanitize_*,顺序反了会留下多余的斜杠。用核心提供的 wp_unslash() 而不是 stripslashes(),因为它能处理数组。

常见函数对照表:

场景 推荐函数
单行文本输入 sanitize_text_field()
多行文本(不允许 HTML) sanitize_textarea_field()
邮箱 sanitize_email()
URL(写入数据库) esc_url_raw()
URL(输出到页面) esc_url()
别名 / slug sanitize_title() 或 sanitize_title_with_dashes()
数组键名、选项名 sanitize_key()
文件名 sanitize_file_name()
HTML 类名、HTML 属性值 sanitize_html_class()
整数(非负) absint()
保留部分 HTML 的正文 wp_kses_post()
完全去除 HTML wp_strip_all_tags()
输出到 HTML 文本节点 esc_html()
输出到 HTML 属性 esc_attr()
输出到 JS 字符串 esc_js()
输出到 textarea 内容 esc_textarea()
输出到内联 style (尽量避免,必要时用白名单)
SQL 语句 $wpdb->prepare()

注意 esc_url() 与 esc_url_raw() 的区别:esc_url() 会把 & 转成 & 以适配 HTML 输出,所以存数据库必须用 esc_url_raw(),用错会导致存进去的 URL 带 HTML 实体,取出来再判断时出错。

保姆级实操

第一步:处理表单提交的完整链路

// 标准顺序:isset 判断 → wp_unslash → sanitize
$name  = isset( $_POST['mmf_name'] )  ? sanitize_text_field( wp_unslash( $_POST['mmf_name'] ) )        : '';
$email = isset( $_POST['mmf_email'] ) ? sanitize_email( wp_unslash( $_POST['mmf_email'] ) )            : '';
$url   = isset( $_POST['mmf_url'] )   ? esc_url_raw( wp_unslash( $_POST['mmf_url'] ) )                : '';
$count = isset( $_POST['mmf_count'] ) ? absint( $_POST['mmf_count'] )                                 : 0;
$note  = isset( $_POST['mmf_note'] )  ? sanitize_textarea_field( wp_unslash( $_POST['mmf_note'] ) )   : '';

注意 absint() 不需要 wp_unslash(),因为它内部会转成整数。

第二步:输出的三种位置,三种函数

<?php
$title = '用户输入的 <b>标题</b> & "引号"';
?>

<!-- 1) 文本节点:用 esc_html -->
<h2><?php echo esc_html( $title ); ?></h2>

<!-- 2) 属性:用 esc_attr -->
<a title="<?php echo esc_attr( $title ); ?>" href="<?php echo esc_url( $url ); ?>">链接</a>

<!-- 3) textarea 内容:用 esc_textarea -->
<textarea><?php echo esc_textarea( $title ); ?></textarea>

<!-- 4) 内联 JS:用 esc_js -->
<script>
  var title = '<?php echo esc_js( $title ); ?>';
</script>

<!-- 5) 需要国际化的场景:用带 _e / __ 的转义版本 -->
<p><?php esc_html_e( '保存成功', 'mmf' ); ?></p>
<p><?php echo esc_html__( '保存成功', 'mmf' ); ?></p>

esc_html_e() 是「转义并输出」,esc_html__() 是「转义并返回」,后者用于需要拼接的场景。

第三步:富文本用 wp_kses

当内容必须保留部分 HTML(比如文章正文、评论)时,不能用 esc_html()(会把标签全显示成文本),也不能不处理(等于放行 XSS)。正确做法:

// 保留文章级别允许的标签(等价于文章正文的规则)
echo wp_kses_post( $content );

// 完全去除所有标签,只留文字
echo wp_strip_all_tags( $content );

// 自定义允许的标签与属性
$allowed = array(
    'a'      => array(
        'href'  => array(),
        'title' => array(),
        'rel'   => array(),
    ),
    'strong' => array(),
    'em'     => array(),
    'br'     => array(),
);
echo wp_kses( $content, $allowed );

也可以基于核心默认规则做增删:

add_filter( 'wp_kses_allowed_html', function ( $tags, $context ) {
    if ( 'post' === $context ) {
        $tags['iframe']['src']     = true;
        $tags['iframe']['loading'] = true;
    }
    return $tags;
}, 10, 2 );

第四步:数据库查询必须参数化

global $wpdb;

// 正确:用 $wpdb->prepare,占位符 %s / %d / %f
$rows = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s AND post_status = %s",
        '%' . $wpdb->esc_like( $keyword ) . '%',
        'publish'
    )
);

// LIKE 里的用户输入必须再用 esc_like 处理,否则 % 和 _ 会被当成通配符

第五步:给数组批量处理

// 递归反转义
$data = wp_unslash( $_POST['mmf_settings'] );

// 批量净化数组里的每个值
$data = array_map( 'sanitize_text_field', (array) $data );

更推荐的做法是显式处理每个键,而不是无脑 array_map,因为不同字段需要不同的净化函数。

第六步:验证结果

# 用安全扫描工具检查插件
wpscan --url https://example.com --enumerate vp --api-token YOUR_TOKEN

# 静态检查:搜索未转义的 echo
grep -rn --include=*.php -E "echo \$|echo \\\$_" wp-content/plugins/your-plugin/

更严格的做法是引入 WordPress 编码规范的 phpcs 规则集(WordPress-Core / WordPress-Extra),它会对未转义的输出报警。

常见踩坑

  1. 入库净化了就以为输出安全:两者防的不是同一件事,必须都做。历史数据、其他插件写入的数据不会被你的入库逻辑净化。

  2. 存 URL 用了 esc_url():会把 & 变成 &#038; 存进数据库,取出来再判断会出错。存用 esc_url_raw(),显示用 esc_url()。

  3. wp_kses_post() 用在属性里:它保留的标签会破坏属性结构。属性位置一律用 esc_attr()。

  4. 忘记 wp_unslash():存进去的数据带多余反斜杠,表现为文本里出现 \'。

  5. esc_html() 用在已包含 HTML 的内容上:用户看到一堆 &lt;p&gt;。富文本场景要用 wp_kses_post()。

  6. 案例: 某插件在后台保存了一条公告文本,入库用了 sanitize_text_field(),前台输出时直接 echo get_option('mmf_notice')。攻击者利用另一个接口的越权漏洞(参见 nonce 与 capability 安全)往这个选项里写入了 <script> 标签,前台所有页面加载即执行,导致管理员会话被劫持。修复方式是输出改为 echo esc_html( get_option('mmf_notice') )。教训是:安全是分层的,某一层的漏洞要靠另一层的防护兜底,输出转义是最后一道防线,绝不能省。

  7. 用 stripslashes() 代替 wp_unslash():前者不处理数组,且没有考虑 WordPress 的魔术引号上下文。

总结

记住这条口诀:输入先 wp_unslash() 再 sanitize_*;输出按位置选 esc_*;富文本用 wp_kses*;SQL 一律 $wpdb->prepare()。

四个高频判断:存 URL 用 esc_url_raw,显示 URL 用 esc_url;数字用 absint;文本节点用 esc_html,属性用 esc_attr;需要保留标签就用 wp_kses_post,不要手动拼 HTML。

把这张对照表固化成团队规范,比事后靠扫描器发现问题有效得多。设置页的完整防护写法见 创建自定义设置页面,而把所有这些函数调用串起来的最小示例,在 从零写一个 WordPress 插件 里。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。