prepared statements能防sql注入,因其在数据库层面将sql结构与参数物理隔离:预编译时生成固定语法树,执行时参数仅作为纯数据绑定,不参与解析;字符串拼接则使用户输入直入sql解析器,必然导致注入。

Prepared Statements 是解决 SQL 注入的首选,因为它从数据库执行层面上切断了“用户输入参与 SQL 解析”这一关键路径。不是靠过滤、转义或经验判断,而是靠协议级隔离——只要用对,就不可能注入。
为什么字符串拼接一定不安全?
因为数据库收到的是一整条待解析的字符串,根本分不清哪部分是代码、哪部分是数据。mysqli_query("SELECT * FROM user WHERE id = " . $_GET['id']) 这种写法,等于把 $_GET['id'] 直接送进 SQL 解析器。哪怕加了单引号:"id = '" . $_GET['id'] . "'",遇到 ' OR '1'='1 也会变成合法语句。
- 数字字段不加引号时,
mysqli_real_escape_string()完全无效 - GBK 等宽字节场景下,
%df%27可绕过转义 -
mysql_*函数在 PHP 8.0+ 已被移除,连连接都建不了
PDO::prepare() 安全的前提条件有哪些?
调用 prepare() 不等于安全。必须同时满足三个硬性条件:
- 所有外部输入必须走占位符(
?或:name),不能出现在 SQL 字符串里 - 禁用模拟预处理:
PDO::ATTR_EMULATE_PREPARES => false - 绑定时指定类型,例如
bindValue(1, $val, PDO::PARAM_STR)或bindValue(':status', $val, PDO::PARAM_INT)
漏掉任一环,比如写了 $pdo->prepare("WHERE name = '{$_POST['name']}'"),那 prepare() 就只是个空壳。
MySQL 服务端预编译到底做了什么?
你发过去的是 "SELECT * FROM users WHERE id = ?",MySQL 立刻做三件事:
- 词法分析:识别出这是 SELECT + 表名 + WHERE + 占位符
- 语法树固化:生成不可变的执行计划,
?的位置已锁定为“纯数据槽位” - 后续只接受二进制参数包,不重新解析 SQL 结构
所以 setInt(1, "1 OR 1=1") 最终查的是 id = 1(字符串被强转为 int 后只剩 1),而不会触发逻辑篡改。这个过程和字符集、编码、引号逃逸完全无关。
容易被忽略的边界情况
表名、列名、排序方向(ORDER BY)、LIMIT 偏移量这些都不能用占位符。它们属于 SQL 结构的一部分,必须靠白名单校验或正则严格限制:
-
ORDER BY后只能是预设字段名:in_array($sort, ['created_at', 'status'], true) -
LIMIT数值必须是intval()后再拼接,且上限需硬编码限制 - 动态表名绝对不能来自用户输入,哪怕经过
preg_replace('/\W/', '', $table)也不行
这些地方一旦放行,Prepared Statements 的防护就出现物理缺口——不是它失效,是你没让它覆盖到。











