转义函数仅对字符串字面量有效,对数字参数、标识符、多语句、二次注入等场景完全无效;唯一可靠方案是全程使用预编译占位符并严格校验标识符白名单。

转义函数只处理字符串字面量,对数字参数完全无效
当用户输入直接拼进 SQL 的 WHERE id = $_GET['id'] 位置时,如果传入的是 1 OR 1=1,mysqli_real_escape_string() 根本不会触发——因为里面没有单引号、反斜杠等它关心的字符。它只在字符串上下文中起作用,而整型字段、布尔表达式、算术运算符这些地方,转义函数连看都不会看一眼。
常见错误现象包括:
- 用
(int)$id强转后仍拼进 SQL 字符串,结果被绕过(如id=1%00 OR 1=1在某些旧环境可能截断) - 把转义后的值再用于
ORDER BY或LIMIT,例如LIMIT 0, $safe_limit,攻击者可填入10; DROP TABLE users - 误以为
addslashes()能替代mysqli_real_escape_string(),但它不感知连接字符集,GBK 下极易被宽字节绕过
标识符(表名、列名)根本无法转义
SQL 中的表名、字段名、数据库名属于标识符,不是字符串字面量,不能加引号,也不能靠转义函数“消毒”。比如 SELECT * FROM ${table_name} 或 ORDER BY ${sort_field},无论你如何处理 $table_name,只要它是外部输入且未经白名单校验,就等于敞开大门。
MySQL 明确禁止对标识符使用参数化占位符:SELECT * FROM ? 会报错;ORDER BY ? 会被当成字符串字面量而非列名解析。此时唯一可靠的做法是:
- 用
in_array($table_name, ['users', 'orders', 'products'], true)白名单硬校验 - 对排序字段做严格枚举:
in_array($sort, ['created_at', 'status', 'updated_at'], true) - 拒绝任何含点号、空格、括号、反引号的输入,它们在标识符中本就不该出现
多语句与注释符让转义彻底失效
一旦后端启用多语句执行(如 mysqli_multi_query()),或前端接口未禁用分号,攻击者就能用 1; DROP TABLE users; -- 直接拆开 SQL 流。此时 mysqli_real_escape_string() 对分号、括号、--、#、/* */ 完全无感——它只管引号和反斜杠,不管语法结构。
更隐蔽的是,即使你没开 multi_query,UNION SELECT、AND (SELECT ...) 这类注入也不依赖引号,转义函数对此类 payload 毫无反应。
性能与兼容性影响也很实际:
- 每次调用
mysqli_real_escape_string()都需访问数据库连接资源,高并发下成瓶颈 - 不同数据库转义规则不一致:PostgreSQL 的
quote_literal()和 MySQL 行为不同,跨库迁移时容易漏掉漏洞 - 若连接字符集未设为
utf8mb4,预处理语句都可能失效,更别说转义函数了
二次注入场景下,转义反而制造虚假安全感
从数据库读出的数据(如 $user_name = $row['name'])若含恶意片段 admin' OR '1'='1,再用 mysqli_real_escape_string() 处理一次,得到的是 admin\' OR \'1\'=\'1。但这个结果若被拼进另一个 SQL 字符串,依然会触发逻辑绕过——尤其当目标字段是整型、或配合正则、JSON 解析等上下文时。
真正的问题不在“有没有转义”,而在于“这个值是否本该具备该语义”。防御必须前置:
- ID 类字段:用
ctype_digit((string)$id)或强转(int)$id后再检查是否 > 0 - 用户名:用
preg_match('/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]{2,20}$/u', $name)限定范围 - 邮箱:必须走
filter_var($email, FILTER_VALIDATE_EMAIL) - 永远不要对从 DB 读出的值做“再次转义”来防注入——校验应在写入前完成
最常被忽略的一点:字符集配置必须三处一致——连接层(PDO DSN 或 mysqli_set_charset())、服务端变量(character_set_client / connection / results)、建表字段(utf8mb4_unicode_ci)。少一处,参数化都可能被绕过,何况转义。

















