必须用 $wpdb->prepare() 一次性处理完整 sql 模板和所有变量,占位符仅限值(%d/%s/%f),表名/字段名须硬编码或白名单校验,且需检查 wp_query 等调用链中用户输入。

必须用 $wpdb->prepare() 替代字符串拼接,且占位符类型、SQL模板范围、标识符处理三者全对,漏洞才真正堵住。
为什么 $wpdb->prepare() 不能只包变量
常见错误是把 $wpdb->prepare() 当成“给变量加引号”的工具,比如:$wpdb->query("SELECT * FROM {$wpdb->prefix}posts WHERE ID = " . $wpdb->prepare("%d", $_GET['id']))。这看似用了 prepare(),但拼接发生在函数调用外,数据库收到的仍是拼接后的原始字符串——prepare() 的上下文早已丢失。
- 正确做法:整个 SQL 模板(含表名、字段名、WHERE 条件)和所有变量,一次性传给
$wpdb->prepare() - 错误写法还包括:
$wpdb->prepare("SELECT * FROM {$wpdb->prefix}posts") . " WHERE ID = %d",占位符在字符串外,prepare()根本不识别 - 后果:代码看起来“用了 prepare”,实则等同裸拼接,CVE-2025-1648 这类漏洞正是这么漏出来的
%d、%s、%f 怎么选不踩坑
占位符不是语法糖,它决定数据如何被强制转换。选错类型等于主动绕过校验:
中文敏感词/违禁词检测与内容合规性检查工具。支持对小红书(Xiaohongshu)、Douyin(抖音)、WeChat(微信)、Weibo(微博)、Bilibili(哔哩哔哩)、Zhihu(知乎)、Taobao(淘宝)、JD.com(京东)等主流平台的禁用词、限用词及高风险词进行文本扫描与合规性分析。
-
%d:仅用于整数。输入"123abc"→ 强转为123;输入"abc123"→ 变成0。ID、status 等字段必须用它 -
%s:用于字符串,做esc_sql()转义,但不改变内容结构。用户名、搜索关键词用它 -
%f:浮点数专用,极少用。多数场景用%d或%s更稳妥 - 典型错误:把
$_GET['id']用%s,虽然能查出结果,但攻击者输1' UNION SELECT ...时,%s不会截断或报错,直接进库执行
表名、字段名、ORDER BY 字段不能用占位符
%s 只能代入值,不能代入标识符(identifier)。否则会被加单引号,变成字符串字面量,导致 SQL 语法错误:
❌ 错误示例:
$wpdb->prepare("SELECT * FROM %s WHERE %s = %s", $table, $column, $value)
→ 实际执行:SELECT * FROM 'wp_posts' WHERE 'post_status' = 'publish'
数据库会报错:Unknown table 'wp_posts'(带引号就不是表名了)。
- 解决方案:表名/字段名必须硬编码,或通过白名单校验后拼接,例如:
$order = in_array($_GET['sort'], ['title', 'date', 'author']) ? $_GET['sort'] : 'date'; - 绝对禁止对不可信来源的字段名做
esc_sql()后直接拼接——esc_sql()只防引号闭合,不防关键字注入 - 动态表前缀可用
{$wpdb->prefix},这是安全的,因为$wpdb->prefix是 WordPress 初始化时确定的常量
修复后还要检查调用链是否干净
插件里一个 WP_Query 实例可能间接触发底层 SQL 拼接。比如 CVE-2022–21661 就是 WP_Query 构造时传入恶意 tax_query,绕过前端验证直达 WP_Tax_Query::clean_query()。这类漏洞不会出现在你写的 $wpdb->prepare() 里,但在调用链深处。
- 检查所有
new WP_Query()、get_posts()、WP_Query::query()的参数来源,尤其是$_POST['query_vars']这类直接透传的入口 - 确认插件是否用
wp_parse_args()处理用户输入——该函数若接收字符串参数,会调用wp_parse_str(),而后者可能解析出恶意键值对 - 核心原则:任何用户可控输入,只要最终参与 SQL 构建,就必须有明确的类型约束或白名单,不能依赖“看起来像数字”这种松散判断(如
is_numeric()+(int)组合,在 LayerSlider CVE-2024-2879 中已被证明可绕过)










