必须用PDO预处理语句替换所有字符串拼接SQL,并禁用模拟预处理;mysqli_real_escape_string无法防御宽字节注入、数字型参数、字符集不匹配及动态字段等场景,白名单校验是处理ORDER BY等动态内容的唯一安全方式。

直接上结论:必须用 PDO 预处理语句替换所有 mysql_query、mysqli_query 字符串拼接,且禁用模拟预处理(PDO::ATTR_EMULATE_PREPARES => false)。PHP 8.2 已彻底移除 mysql_* 扩展,继续用字符串拼接等于主动交出数据库密码。
为什么不能只加个 mysqli_real_escape_string 就完事
老代码常见补丁式写法:$sql = "SELECT * FROM users WHERE name = '" . mysqli_real_escape_string($conn, $_GET['name']) . "'"; —— 这在 PHP 8.2 下不仅无效,还危险。
- 宽字节注入仍可绕过:比如
%df%27在gbk编码下会被解析为单引号,real_escape_string对它无感 - 数字型参数不加引号时,转义完全失效:
"id = " . $_GET['id']中输入1 OR 1=1不触发任何转义逻辑 - PHP 8.2 的
mysqli默认字符集是utf8mb4,但若连接未显式声明,real_escape_string可能按latin1处理,导致双字节截断漏洞 - 它无法防御动态表名、列名、排序字段等场景,而这些在旧系统里高频出现(如后台列表页的
ORDER BY $_GET['sort'])
PDO 迁移时最常踩的三个坑
不是写了 prepare 就安全了。很多重构后仍被扫出漏洞,问题就出在这几处:
-
PDO::ATTR_EMULATE_PREPARES必须设为false:默认是true,PDO 会在 PHP 层“模拟”预处理,把占位符替换成转义后的值再发给 MySQL——这等于又回到了字符串拼接,完全失去预处理意义 - 命名参数不能重复使用:写
":id AND id != :id"会报错,必须改成":id AND id != :id2"并分别绑定,否则执行时静默失败或查不到数据 -
LIKE查询的通配符必须在 PHP 层拼,不能塞进占位符:"WHERE name LIKE :keyword"+ 绑定"%admin%"是对的;绑定"admin"再让 SQL 拼"%:keyword%"是错的——后者会被当成字面量,查不出结果
动态字段(如 ORDER BY、表名)怎么处理才不拼接
这类需求没法用占位符,硬拼等于开门揖盗。唯一安全做法是白名单校验 + 显式映射:
立即学习“PHP免费学习笔记(深入)”;
- 禁止任何形式的变量直插:
"ORDER BY " . $_GET['sort']或"ORDER BY {$sort}"全部删掉 - 定义允许的字段和方向:
$allowedSorts = ['created_at' => 'created_at', 'status' => 'status']; $allowedOrders = ['ASC', 'DESC']; - 从输入中提取并校验:
$sort = $allowedSorts[$_GET['sort']] ?? 'created_at'; $order = in_array($_GET['order'], $allowedOrders) ? $_GET['order'] : 'ASC'; - 拼接时仅用校验后的变量:
"ORDER BY {$sort} {$order}"—— 此时变量内容已锁定在白名单内,无注入风险
真正难的不是写对一行 prepare,而是把整个项目里所有 SQL 构造点都找出来、分类、打标、逐个替换。尤其要注意那些藏在 include 文件、DAO 类方法、甚至 JS 拼接的 AJAX URL 里的隐性拼接点——它们往往比明面上的 query() 更容易被漏掉。



















