编程 PHP 8.6 的 quote_string():real_escape_string() 在 NO_BACKSLASH_ESCAPES 下失效的原因

2026-10-31 21:04:32

PHP 8.6 的 quote_string():real_escape_string() 在 NO_BACKSLASH_ESCAPES 下失效的原因

real_escape_string() 只负责把危险字符转义,不负责加引号:函数返回的是一段“已经转义但还没有被引号包起来”的文本,是否在它外面套上引号、套哪一种引号,完全由你在拼接 SQL 时决定。这个分工在默认 sql_mode 下没有问题,一旦连接启用了 NO_BACKSLASH_ESCAPES,就会失效。

PHP 8.6(GA 19 Nov 2026)为 mysqli 增加了 mysqli::quote_string() 和过程式别名 mysqli_quote_string():转义和加引号一步完成,并且引号风格按当前连接的 sql_mode 选择。

来源:php.net、startwithphp.com

real_escape_string() 的转义依赖一个前提

real_escape_string() 的做法是在字符前添加反斜杠:' → \'," → \",\ → \\,以及 NUL、\n、\r、\x1a。它能按这种方式生效,前提是 MySQL 解析器把反斜杠视为转义符。

默认模式下确实如此:

$name = "O'Reilly";
$sql  = "SELECT * FROM users WHERE name = '" . $mysqli->real_escape_string($name) . "'";
// 拼出:... WHERE name = 'O\'Reilly'   —— 合法,反斜杠让 ' 失去闭合作用

NO_BACKSLASH_ESCAPES 会关闭这个前提:反斜杠退化为普通字符,SQL 字符串的唯一界定方式是单引号自身的重复写('')。上面同一段代码就会生成下面这条语句:

$mysqli->query("SET SESSION sql_mode = 'NO_BACKSLASH_ESCAPES'");

$name = "O'Reilly";
$sql  = "SELECT * FROM users WHERE name = '" . $mysqli->real_escape_string($name) . "'";
// 拼出:... WHERE name = 'O\'Reilly'
// 反斜杠不再是转义符,字符串在 O\ 之后的 ' 处提前闭合,
// 剩下的 Reilly' 落到了 SQL 语句层,成为可被构造的语句片段

转义函数返回了它认为安全的结果,拼接方也按常规加了单引号,但两者的前提不一致。这类问题不报错、不抛异常,在默认配置的测试环境里也复现不出来,只有当某个连接(或被中间件改写过 sql_mode)启用 NO_BACKSLASH_ESCAPES 时才会出现。

quote_string() 把“转义”和“加引号”合并成一步

新函数的返回值是完整的字面量:调用方直接把它拼进 SQL,不需要也不应该再在外面补引号。引号风格由连接当前的 sql_mode 决定:

$name = "O'Reilly";

// 默认 sql_mode
$sql = "SELECT * FROM users WHERE name = " . $mysqli->quote_string($name);
// ... WHERE name = 'O\'Reilly'

// NO_BACKSLASH_ESCAPES
$sql = "SELECT * FROM users WHERE name = " . $mysqli->quote_string($name);
// ... WHERE name = 'O''Reilly'

// 过程式写法等价
$sql = "SELECT * FROM users WHERE name = " . mysqli_quote_string($mysqli, $name);

关键在于引号由函数决定,而不是由调用方写死。real_escape_string() 时代真正危险的地方,往往在于转义按一种模式进行,引号却按另一种习惯补上;quote_string() 把这两件事放在同一个函数里,依据同一份连接状态。

升级检查清单

替换之前,先查清现有代码里有多少地方在手工管理引号。

# 1. 找出所有手工转义点,逐个确认返回值外面是否手工补了引号
grep -rnE --include='*.php' 'real_escape_string|mysqli_escape_string|->escape_string' .

# 2. 找出对 sql_mode 的假设:全局配置、会话设置、连接初始化串
grep -rnE --include='*.php' 'NO_BACKSLASH_ESCAPES|SET (SESSION )?sql_mode|sql_mode' .

# 3. 找出转义结果直接被双引号字符串包住的写法,这类最容易被 sql_mode 影响
grep -rnE --include='*.php' '"\s*\.\s*\$?[A-Za-z_]*->?real_escape_string' .

替换时容易出错的两个地方:

  • 不要重复加引号。 quote_string() 已经带上了引号,原来 SQL 模板里的 '...' 要去掉,否则会变成 ''O''Reilly''。
  • 不要缓存返回值。 结果依赖连接当时的 sql_mode;会话级设置中途被改动后,旧结果不再适用。

动态拼接场景的边界:预处理仍是首选

quote_string() 解决的是“值”这一层的安全问题。表名、列名、ORDER BY 方向、LIMIT 偏移这些位置不能靠引号包裹来保护,仍然需要走白名单校验。

只要语句形状固定、只有值在变,就应该用预处理语句:服务端准备好语句后,参数以独立数据的形式传输,不参与 SQL 文本解析,因此不受任何 sql_mode 影响:

$stmt = $mysqli->prepare('SELECT * FROM users WHERE name = ?');
$stmt->bind_param('s', $name);
$stmt->execute();

quote_string() 面向的是那些预处理覆盖不到、或改造成本很高的地方:动态拼出的 IN (...) 列表、由多段值拼成的条件、老代码里还没迁移走的字符串拼接。在这些地方,它比 real_escape_string() 正确,但不代表可以把预处理语句改回手工拼接。

复制全文 生成海报 PHP MySQL 安全 SQL注入 版本升级

推荐文章

程序员茄子在线接单