代码 real_escape_string 在双引号里并不安全:PHP 8.6 的 mysqli_quote_string 连引号一起处理

2026-11-06 21:03:56

real_escape_string 在双引号里并不安全:PHP 8.6 的 mysqli_quote_string 连引号一起处理

PHP 8.6 新增了 mysqli::quote_string() 和过程式别名 mysqli_quote_string()。这个函数做两件事:按当前连接的字符集转义字符串,再根据连接的 SQL 模式选一种合适的引号样式,把值包裹起来,返回已经带引号的字面量。它修复的场景是:mysqli_real_escape_string() 的输出被塞进双引号字符串,而会话开了 NO_BACKSLASH_ESCAPES 时留下的注入面。

预处理语句仍然是推荐路径,quote_string() 只是把「不得不拼接字面量」时的默认做法从易错变正确。

转义函数为什么只在特定引用符下成立

mysqli_real_escape_string() 的语义是:考虑连接当前字符集,转义 SQL 语句中的特殊字符。它不添加外层引号,转义方式是把特殊字符前面加上反斜杠,' 变 \'," 变 \"。

这套做法成立的前提是「反斜杠是转义符」。一旦有人在会话里执行:

SET sql_mode='NO_BACKSLASH_ESCAPES';

反斜杠就退化成一个普通字符。此时 \' 在字符串里表示「反斜杠 + 引号」,引号会照常闭合字符串,转义等于没做。

两种引号的处境并不对等:

  • 单引号包裹时,MySQL 还有第二种写法,把内部的 ' 双写成 ''。但 real_escape_string() 生成的是 \' 而不是 '',所以它同样失效——只是理论上还有救。
  • 双引号包裹时,没有任何替代写法可用("" 在字符串里不表示转义的双引号,在 ANSI_QUOTES 下双引号还直接变成标识符引用符)。\" 一失效就是死路。

可复现的注入样例

$mysqli = new mysqli('127.0.0.1', 'app', 'secret', 'test');
$mysqli->set_charset('utf8mb4');
$mysqli->query("SET sql_mode='NO_BACKSLASH_ESCAPES'");

$name = 'a" OR 1=1 -- ';
$sql  = 'SELECT * FROM users WHERE name = "' . $mysqli->real_escape_string($name) . '"';
echo $sql;

输出的语句是:

SELECT * FROM users WHERE name = "a\" OR 1=1 -- "

在 NO_BACKSLASH_ESCAPES 下,\" 里的反斜杠是字面量,引号在 a\ 之后就把字符串闭合了,剩下的 OR 1=1 -- 成为真正的 SQL。同一段代码在默认 sql_mode 下是安全的,这正是它难被发现的原因:安全性取决于运行时会话状态,而不取决于调用点的代码长得对不对。

quote_string 的用法:返回值已经带引号

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

$quoted = $mysqli->quote_string('a" OR 1=1 -- ');
$sql = 'SELECT * FROM users WHERE name = ' . $quoted;
// SELECT * FROM users WHERE name = 'a" OR 1=1 -- '

这里连接处于 NO_BACKSLASH_ESCAPES,函数选了单引号样式,内部的 ' 按 '' 规则处理;模式切回默认后,它会改用反斜杠转义并加引号。

需要特别注意:不要在外面再手动加引号。

// 错误:变成两层引号
$sql = 'SELECT * FROM users WHERE name = "' . $mysqli->quote_string($name) . '"';

quote_string() 已经返回 '...' 这样的完整字面量,再套一层双引号,得到的要么是语法错误,要么是一个被当成字符串拼接的空串 + 原串,值直接变了。

只适用于字面量拼接

quote_string() 产出的是字符串字面量,不能用来拼表名、列名、ORDER BY 字段这类标识符。标识符位置需要白名单或反引号转义:

$allowed = ['id', 'name', 'created_at'];
$col = in_array($_GET['sort'] ?? '', $allowed, true) ? $_GET['sort'] : 'id';
$sql = 'SELECT ' . $col . ' FROM users ORDER BY ' . $col;

把 'users' 放在表名的位置,SQL 会把字符串字面量当成表名,直接报语法错。

8.5 及以下的替代写法

addslashes() 不能当作替代:它不感知连接字符集,也不感知 sql_mode,在多字节字符集下存在已知的绕过方式。

需要手工拼接字面量时,只能自己读取会话的 sql_mode 并据此选引号样式:

function quote_literal(mysqli $db, string $s): string {
    $row = $db->query('SELECT @@SESSION.sql_mode AS m')->fetch_assoc();
    if (strpos($row['m'], 'NO_BACKSLASH_ESCAPES') !== false) {
        return "'" . str_replace("'", "''", $s) . "'";
    }
    return "'" . $db->real_escape_string($s) . "'";
}

这段代码每次都会多一次查询。能改用预处理语句的地方,还是用预处理语句更省事。

复制全文 生成海报 PHP MySQL SQL注入 安全

推荐文章

程序员茄子在线接单