返回博客

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 '%%%'

三个 % 等价于一个 %,匹配所有行。攻击者用这一个字符就能:

  1. 数据越权:拿到不属于自己的数据(搜索接口通常没有二次鉴权)
  2. DoS:绕过索引,强制全表扫描,打崩慢查询队列
  3. 枚举探测:用 a%b% 逐字母枚举,暴力猜测其他用户的字段值
坑:参数绑定 ≠ LIKE 安全

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);
正解:PHP 侧先 addcslashes,再拼 LIKE 模式

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

输出的每一行都值得逐一确认,尤其是带 phoneorder_nomobile 这类敏感字段的查询。

一句话外卖

PDO 参数绑定防的是 SQL 注入,LIKE 通配符防的是模式注入——这是两道门,只锁一道等于没锁。