PDO 参数绑定救不了你——MySQL LIKE 通配符注入的隐藏盲区
做后台管理系统安全审计时,我扫到了一类极容易被忽视的问题:列表页的关键字搜索接口,全部直接把用户输入拼进 LIKE,但开发同学认为"已经用了参数绑定,不会有注入"。
这个认知是错的。参数绑定和 LIKE 通配符是两个独立的安全边界,两者互不覆盖。
现象
某搜索接口,用户输入 % 作为关键字,返回了全表数据——包括不属于该用户的记录。
输入 _ 则命中了大量无关行(单字符通配)。
QA 压测时偶发响应超时,根因是全表扫描让查询时间从 5ms 涨到 3s+。
根因
MySQL LIKE 有两个元字符:
%— 匹配任意长度的任意字符串_— 匹配任意单个字符
PDO / MySQLi 的参数绑定(? 占位)只负责转义字符串定界符(单引号、反斜杠),不会处理 % 和 _ 在 LIKE 模式中的语义。所以下面这段代码,哪怕用了绑定,依然是有漏洞的:
// 错误示例——参数绑定救不了 LIKE 元字符
$keyword = $request->post('keyword');
$query->where('name', 'like', '%' . $keyword . '%');当 $keyword === '%' 时,MySQL 实际执行的是:
WHERE name LIKE '%%%'三个 % 等价于一个 %,匹配所有行。攻击者用这一个字符就能:
- 数据越权:拿到不属于自己的数据(搜索接口通常没有二次鉴权)
- DoS:绕过索引,强制全表扫描,打崩慢查询队列
- 枚举探测:用
a%、b%逐字母枚举,暴力猜测其他用户的字段值
PDO 的 bindValue / execute([...]) 只防 SQL 注入(字符串定界符逃逸),对 LIKE 的 % 和 _ 元字符完全不做处理。用了绑定依然需要在 PHP 侧手动转义。
正解
在把关键字送进 LIKE 之前,先在 PHP 侧转义元字符:
private function escapeLike(string $value): string
{
// 同时转义反斜杠本身,防止 \% 被误解释
return addcslashes($value, '%_\\');
}
// 使用
$keyword = trim($this->request->post('keyword', ''));
if ($keyword !== '') {
$safe = $this->escapeLike($keyword);
$query->where('name', 'like', '%' . $safe . '%');
}如果用的是原生 SQL + 绑定:
$safeKeyword = addcslashes(trim($keyword), '%_\\');
$db->query(
'SELECT * FROM orders WHERE title LIKE ? OR order_no LIKE ?',
['%' . $safeKeyword . '%', '%' . $safeKeyword . '%']
);另外加一道长度截断,防止攻击者构造超长模式字符串(即使转义了,正则引擎仍要消耗 CPU):
$keyword = mb_substr(trim($keyword), 0, 50);用 addcslashes($value, '%_\\') 或 str_replace(['%','_'], ['\\%','\\_'], $value) 在绑定前转义元字符。两种写法等价,推荐封成私有方法或全局 helper,整个项目统一调用,不要散落各处内联。
审计 grep
批量扫项目里哪些 LIKE 还没转义:
grep -rn "->where.*'like'.*\$.*request\|'like'.*'%' \." app/api/controller/ \
| grep -v escapeLike \
| grep -v escape_like \
| grep -v str_replace \
| grep -v addcslashes输出的每一行都值得逐一确认,尤其是带 phone、order_no、mobile 这类敏感字段的查询。
一句话外卖
PDO 参数绑定防的是 SQL 注入,LIKE 通配符防的是模式注入——这是两道门,只锁一道等于没锁。