必须使用 $wpdb->prepare() 防止 sql 注入,严禁拼接 $_get/$_post 值;占位符 %d/%s/%f 需严格匹配类型;表名、字段名等标识符不可用占位符,须白名单校验;prepare() 不替代权限控制,需配合 nonce、角色检查等。

必须用 $wpdb->prepare(),不能拼接字符串
直接把 $_GET 或 $_POST 的值塞进 SQL 字符串里,等于给攻击者递刀。比如写成 "WHERE id = " . $_GET['id'],用户传 1 OR 1=1 就能查出全表数据。$wpdb 不会自动过滤任何东西,它只忠实地执行你给它的 SQL。
正确做法是:整个 SQL 模板 + 所有变量,一次性交给 $wpdb->prepare():
$wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}posts WHERE ID = %d AND post_status = %s", $id, $status));
- 错误写法包括:
$wpdb->prepare("SELECT * FROM ... WHERE ID = " . %d, $id)(拼接破坏上下文) -
$wpdb->prepare("SELECT * FROM ...") . " WHERE ID = %d"(占位符在 prepare 外,完全失效) - 只对部分参数用
prepare(),其余靠intval()或esc_sql()补救——这不算防护,只是侥幸没被绕过
%d、%s、%f 必须按类型严格匹配
占位符不是装饰,选错就等于开后门。例如 ID 字段用 %s 看似能跑通,但 1' UNION SELECT 这类 payload 会被原样保留,而 %d 会强制转成整数,1' UNION SELECT 直接变成 1。
-
%d:仅用于整数,内部调用(int),abc123→0,123abc→123 -
%s:用于字符串,走esc_sql()转义,但不改变长度或结构,适合用户名、标题等自由文本 -
%f:浮点数专用,极少用;多数场景该用%d或%s,混用会导致精度丢失或截断
表名、字段名、ORDER BY 不能用占位符
%s 只能代入值,不能代入标识符。否则会被加单引号,变成字符串字面量,SQL 直接报错:
❌ $wpdb->prepare("SELECT * FROM %s WHERE %s = %s", $table, $column, $value)
→ 实际执行:SELECT * FROM 'wp_posts' WHERE 'post_status' = 'publish'
正确方式只有两个:
- 白名单硬编码:比如排序字段只允许
post_date、post_title、ID,用in_array($order, ['post_date', 'post_title', 'ID'])校验后再拼接 - 可信来源才用
esc_sql()手动处理,例如插件自己定义的固定字段映射表,且不接受用户直接输入
动态表名更危险,必须从预设列表中取,绝不能来自 $_REQUEST。
prepare() 不是银弹,权限和上下文同样关键
$wpdb->prepare() 只解决“值污染”问题,它不校验用户有没有权限执行这条查询,也不阻止你把高危操作暴露给未登录用户。
- CVE-2022-4447(Fontsy 插件)和 CVE-2024-10400(Tutor LMS)都是典型的“用了
prepare()却没做权限控制”的案例:AJAX 接口未校验 nonce、未检查用户角色,攻击者未登录就能触发带prepare()的查询 - 如果查询本身涉及删除、更新或跨表关联,光防注入不够,还得叠加
current_user_can()、check_ajax_referer()、is_admin()等上下文判断 - 某些场景(如
WP_Query构造)根本绕不开 SQL 拼接逻辑,这时必须彻底禁用用户可控字段,或改用 WordPress 原生查询接口(如get_posts())替代手写 SQL
最易被忽略的一点:漏洞常不出现在主查询逻辑里,而出现在日志记录、调试输出、缓存键生成等“辅助代码”中——只要那里拼了 SQL,且参数来自用户,就一样危险。











