安全加固
这一课你将能做到
- 建立「进站净化 → 存库参数化 → 出站转义」的全链路防线,对 XSS/SQL 注入/CSRF 三大攻击逐一落地防御
- 用 nonce + current_user_can 写出防伪造、防越权的表单与 AJAX 处理
- 交出一份生产环境加固清单:密钥、权限、登录保护、更新策略
为什么要学
WP 是全球攻击面最大的 PHP 应用之一:43% 的网站份额让它成为自动化扫描的头号目标。绝大多数被黑站点不是因为 WP 本身,而是插件漏洞 + 开发者不做转义/校验。安全不是选修课——你的站点挂了挂马、SEO 被拉黑,损失立竿见影。
人话:三类攻击各管一段路。SQL 注入管「进」(用户输入拼进了 SQL);XSS 管「出」(脏数据被原样输出,在别人浏览器里执行);CSRF 管「请求来源」(坏人页面替已登录用户发请求)。WP 给你的对策分别是 prepare、esc_* 家族、nonce + 能力检查。
先建立图像
用户输入 ──① sanitize 净化──→ 入库($wpdb->prepare / API)
│
输出到页面 ←──③ esc_* 转义 ←───────┘
写操作请求 ──② nonce 验来源 + current_user_can 验身份──→ 执行
口诀:进净化、出转义、写校验。三道闸缺一不可。
跟着做
步骤 1:出站转义家族(escape)
// 纯文本内容
echo '<p>' . esc_html( $user_input ) . '</p>';
// HTML 属性值(引号安全)
echo '<div class="' . esc_attr( $meta['level'] ) . '">';
// URL(协议白名单,防 javascript: 注入)
echo '<a href="' . esc_url( $meta['link'] ) . '">查看</a>';
// 内联 JS 数据(配 wp_json_encode)
echo '<script>var CFG = ' . wp_json_encode( $cfg ) . ';</script>';
// 富文本:白名单保留标签
echo wp_kses_post( $user_html ); // 允许 a/p/strong 等,剔掉 script
原则是 late escaping:在输出点转义,而不是信任「入库时已净化」——数据来源可能变化,输出点才是最后一道闸。
步骤 2:入站净化家族(sanitize)
$course = array(
'title' => sanitize_text_field( wp_unslash( $_POST['title'] ?? '' ) ),
'email' => sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ),
'level' => sanitize_key( wp_unslash( $_POST['level'] ?? '' ) ), // 小写字母数字下划线
'duration' => absint( wp_unslash( $_POST['duration'] ?? 0 ) ),
);
update_post_meta( $post_id, 'course_meta', $course );
wp_unslash 剥掉 WP 兜底的转义层(magic quotes 遗产),再交给对应 sanitize 函数。
步骤 3:nonce + 能力检查完整链(自定义字段保存)
add_action( 'add_meta_boxes', function () {
add_meta_box( 'qrocmq_course_box', '课程设置', function ( WP_Post $post ) {
wp_nonce_field( 'qrocmq_save_course', 'qrocmq_nonce' ); // ① 表单埋 nonce
$level = get_post_meta( $post->ID, 'level', true );
echo '<select name="qrocmq_level">';
foreach ( array( 'beginner' => '入门', 'advanced' => '进阶' ) as $k => $v ) {
printf( '<option value="%s" %s>%s</option>',
esc_attr( $k ), selected( $level, $k, false ), esc_html( $v ) );
}
echo '</select>';
}, 'course' );
} );
add_action( 'save_post', function ( int $post_id, WP_Post $post ): void {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
if ( wp_is_post_revision( $post_id ) ) return;
// ② 验 nonce:请求是不是从我们的表单发出的
if ( ! isset( $_POST['qrocmq_nonce'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['qrocmq_nonce'] ) ), 'qrocmq_save_course' ) ) {
return;
}
// ③ 验身份:当前用户有没有权改这篇文章
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
update_post_meta( $post_id, 'level', sanitize_key( wp_unslash( $_POST['qrocmq_level'] ?? '' ) ) );
}, 10, 2 );
AJAX 版本:处理函数开头 check_ajax_referer( 'qrocmq_action', 'nonce' ),再接 current_user_can。
步骤 4:SQL 注入防线
global $wpdb;
// 正确:占位符
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s",
$type, // 用户可控的值 → 走占位符
'publish'
)
);
// 动态排序字段不能参数化 → 白名单
$allowed = array( 'date', 'title' );
$orderby = in_array( $_GET['orderby'] ?? '', $allowed, true ) ? $_GET['orderby'] : 'date';
步骤 5:环境加固清单
wp-config.php:
define( 'DISALLOW_FILE_EDIT', true ); // 关闭后台在线改代码
define( 'FORCE_SSL_ADMIN', true ); // 后台强制 HTTPS
运维动作(部分给命令,详见下一课 WP-CLI):
- 文件权限:目录 755、文件 644,wp-config.php 600
- 登录保护:限流插件(如 Limit Login Attempts)防爆破
- 更新纪律:核心与插件及时更新(多数漏洞有补丁),不装盗版主题
- 隐藏版本号:
remove_action( 'wp_head', 'wp_generator' ) - 定期改盐值可在事故后让全部登录态失效
讲清楚要点
- sanitize 与 escape 方向相反:净化管入口(格式合法化),转义管出口(上下文安全化)。只做其一都留缺口。
- nonce 不是权限系统:它证明请求来自你的表单(防 CSRF),不证明用户是谁、能不能做——身份判断永远交给 current_user_can。
- 最小权限原则:给编辑的账号别给管理员;代码里的能力检查用最窄的能力(edit_post 而非 manage_options)。
- WP 的攻击面排序:过时插件 > 弱口令 > 主题漏洞 > 核心漏洞。更新纪律比任何防火墙插件都有效。
- unserialize 反序列化是高危:不 unserialize 任何用户可控输入(对象注入),配置传输用 JSON。
你可能会懵的地方
-
nonce 为什么会失效? 有效期 24 小时(两拍),且与用户、动作绑定。页面放久了提交失败属正常——失败后让用户刷新重试,别把 nonce 当持久令牌。
-
esc_html 和 wp_kses_post 怎么选? 输出应为纯文本(标题、名字)→ esc_html(全部 HTML 变实体);输出确需富文本(用户写的简介)→ wp_kses_post 白名单过滤。
-
nonce 校验通过了还查 current_user_can,是不是重复劳动? 不是。nonce 防「陌生人替你提交」,能力检查防「登录用户做超出角色的事」——两个不同维度的威胁。
-
密码忘了进不去后台怎么办?
wp user update admin --user_pass=新密码(WP-CLI),或数据库层面重置 user_pass 字段(MD5 函数会让 WP 自动升级哈希)。 -
被黑后的恢复顺序? 隔离(关站/断写)→ 换全部密钥与密码 → 与干净备份/官方核心比对找后门 → 重装核心与可信插件 → 排查数据库注入残留 → 复盘更新策略。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| $_GET 直接拼进 SQL | SQL 注入 | $wpdb->prepare 占位符 |
| 只在入库时 strip_tags,输出不管 | 数据源变化即 XSS | late escaping 输出点转义 |
| 校验了 nonce 却不查能力 | 低权限用户伪造请求成功 | nonce + current_user_can 成对出现 |
| 把密钥写进代码库 | 密钥随仓库泄露 | wp-config/环境变量,仓库排除 |
| unserialize( $_POST['data'] ) | 对象注入 RCE | 用 JSON + 白名单字段校验 |
卡点急救
- 后台无限跳回登录页:浏览器 cookie 被拒或站点 URL 与实际域不符;检查 wp-config 的 WP_HOME/WP_SITEURL 与 HTTPS 混用问题。
- 提交表单总提示 nonce 失效:缓存层把 A 用户的页面连 nonce 缓存给了 B——带 nonce 的表单页面必须排除在页面缓存外。
- 某插件被曝漏洞但不能立即升级:先停用该插件功能入口,用应用密码收紧 API,监控日志;尽快在测试环境验证升级。
面试常问
-
WP 中防御 XSS 的完整方案? 输出点按上下文转义:esc_html/esc_attr/esc_url/wp_kses_post,late escaping 原则;配合入口净化 sanitize_* 与 CSP 兜底。
-
nonce 的工作原理与局限? 基于动作、用户、时间(24h)生成的哈希令牌,随表单提交并在服务端 wp_verify_nonce 验证,防 CSRF;它不验证用户身份与权限,也不适用于公开缓存页面。
-
如何评估一个第三方插件是否安全? 看更新时间与活跃安装量、官方仓库而非第三方下载、查已知漏洞库(Patchstack/WPVulnDB)、代码层面检查是否 prepare/转义/能力检查、上线前在测试环境审查。
自测三问
- 写出「进净化、出转义、写校验」分别对应的函数族与典型 API。
- nonce 与 current_user_can 各防什么威胁?为什么缺一不可?
- 动态 ORDER BY 字段为什么不能进 prepare?正确做法是什么?
小结一句话
安全 = 方向正确的三道闸:入口 sanitize、出口 esc_*(late escaping)、写操作 nonce+能力检查,再加更新纪律与最小权限——自动化攻击扫不动纪律严明的站点。
延伸
- WP 安全 API 手册(数据验证)
- 官方安全文档
- 下一课:性能与运维——三层缓存、CDN 与 WP-CLI 日常运维。