WordPress SQL安全必须用$wpdb->prepare()处理用户输入,%d/%s严格区分类型,表名列名需白名单校验,dbDelta()须加载upgrade.php并规范SQL格式。

直接拼接变量进SQL就是漏洞入口
只要出现 $_GET、$_POST 或任何用户可控输入被插进 SQL 字符串里,就等于给攻击者开了后门。比如 "SELECT * FROM {$wpdb->prefix}posts WHERE ID = " . $_GET['id'] 这种写法,传入 1 OR 1=1 就能绕过条件查出全部文章;传入 1'; DROP TABLE wp_users; -- 更可能清空用户表。
WordPress 的 $wpdb 不会自动过滤或转义你塞进去的字符串——它只负责执行。安全不是靠运气,而是靠强制约束。
- 所有含变量的查询必须走
$wpdb->prepare() - 禁止用双引号或单引号拼接变量,哪怕加了
esc_sql()也不行(它只是转义,不能防类型混淆) - 不要信任“这个参数我前端校验过了”,后端必须独立验证
$wpdb->prepare() 占位符选错会失效
占位符不是语法糖,是类型契约:%d 强转整数,%s 做字符串转义,%f 处理浮点。混用会导致防护形同虚设。
比如把用户传来的 ID 当字符串用 %s:攻击者输 123' UNION SELECT user_login, user_pass FROM wp_users --,%s 会保留单引号和空格,语句照样执行;而用 %d,整个字符串会被转成 0,查询直接失败。
- ID、status、limit、offset 等数值型字段一律用
%d - 用户名、邮箱、搜索关键词等文本内容用
%s -
%f极少需要,浮点运算在 WordPress 后端场景基本可忽略 - 不要为了“省事”把所有变量都套
%s,那是自欺欺人
表名和字段名不能放占位符
$wpdb->prepare() 只处理值,不处理标识符。把表名或排序字段塞进 %s,结果是带引号的字符串字面量,SQL 直接报错:
❌ 错误:$wpdb->prepare("SELECT * FROM %s WHERE %s = %s", $table, $col, $val) → 实际执行 SELECT * FROM 'wp_mytable' WHERE 'status' = '1'
✅ 正确做法分两步:先白名单校验,再硬编码或手动转义(仅限可信来源):
- 动态表名/字段名必须来自预设数组:
$allowed_tables = ['wp_posts', 'wp_users']; $table = in_array($user_table, $allowed_tables) ? $user_table : 'wp_posts'; - 动态 ORDER BY 字段必须限定范围:
$order = in_array($_GET['sort'], ['post_date', 'post_title', 'ID']) ? $_GET['sort'] : 'post_date'; $sql = "ORDER BY $order DESC"; - 绝对不要对表名列名调用
esc_sql()后直接拼接——它只为值设计,对标识符无效且误导性强
dbDelta() 创建表时容易漏掉 upgrade.php 加载
自定义表创建不是写个 CREATE TABLE 就完事。dbDelta() 函数藏在 wp-admin/includes/upgrade.php 里,这个文件默认不加载。插件激活钩子里忘了 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');,调用 dbDelta() 就会报错或静默失败。
更隐蔽的问题是:如果表已存在但结构有变更(比如新增字段),dbDelta() 必须看到完整、格式严格的 SQL 才能识别差异。常见坑包括:
- SQL 中每行只能有一个字段定义,
PRIMARY KEY必须独占一行 - 字段定义末尾不能有逗号,但最后一行字段后要跟一个空行
- 引擎和字符集必须显式写出:
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4,不能依赖 MySQL 默认值 - 表名务必用
$wpdb->prefix拼接,否则多站点环境会建错库
复杂逻辑别堆在激活钩子里——拆成独立函数,加日志或 error_log() 输出 dbDelta() 返回值,才能看清是没执行、执行失败,还是根本没进到那段代码。











