预处理防注入需同时满足三条件:所有用户输入进占位符、禁用模拟预处理(pdo::attr_emulate_prepares=>false)、参数全部绑定;缺一不可。

预处理语句不是“学完语法就能防注入”,而是必须同时满足三个硬性条件:SQL 字符串里**所有用户输入都进占位符**、连接时**禁用模拟预处理(PDO::ATTR_EMULATE_PREPARES => false)**、执行前**所有参数都经过绑定(execute()、bindValue() 或 bindParam())**。少一个,防护就失效。
为什么 prepare() 单独调用不防注入
常见错误是写成:$pdo->prepare("SELECT * FROM user WHERE name = '{$_POST['name']}'") —— 这根本没用占位符,prepare() 只是返回一个空壳对象,实际还是字符串拼接。数据库收到的仍是被污染的完整 SQL。
- 真正起效的链条是:
prepare()生成带?或:name的语句 → 再用execute()、bindValue()等传值 -
bindValue(1, $_POST['name'], PDO::PARAM_STR)中的1是位置索引,不是数组键;命名占位符如:status,绑定时键名必须带冒号且大小写严格匹配 - 如果
prepare()返回false或抛异常,说明 SQL 本身有语法错误,不是参数问题——得先修语句,再谈绑定
PDO::ATTR_EMULATE_PREPARES => false 必须显式设置
PHP 默认开启模拟预处理(true),即在 PHP 层自己做字符串替换,不交由 MySQL 服务端编译。这在宽字节环境(如未设 charset=utf8mb4 的旧配置)下极易被绕过。
- 连接时必须写全:
new PDO($dsn, $user, $pass, [PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]) - 验证是否生效:
var_dump($pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES))输出false才算成功 - 不设这个,哪怕你用对了占位符和
execute(),攻击者仍可能通过编码漏洞注入
哪些地方死活不能用占位符
ORDER BY、GROUP BY、表名、字段名、LIMIT 后的 OFFSET 关键字——这些属于 SQL 语法结构(identifier),不是“值”(value),预处理机制根本不支持绑定。
- 错误写法:
"SELECT * FROM {$table} ORDER BY {$sort_field}",哪怕只拼一个变量,整条语句就不可信 - 正确做法:白名单校验 + 硬拼。例如排序字段:
in_array($sort, ['id', 'title', 'created_at'], true),注意第三个参数true启用严格类型检查 - 排序方向只接受
'ASC'或'DESC',且需统一转大写:strtoupper($dir) === 'ASC' ? 'ASC' : 'DESC'
bindValue() 比 execute() 数组传参更可控
execute([':name' => $name]) 看似简洁,但容易因键名拼错、空格、大小写或键缺失导致静默失败(尤其关闭异常模式时)。bindValue() 显式控制每个参数,类型可指定,调试更直接。
- 推荐写法:
$stmt->bindValue(':name', $_POST['name'], PDO::PARAM_STR);整数用PDO::PARAM_INT,布尔用PDO::PARAM_BOOL -
bindParam()绑定的是变量引用,适合循环中反复执行同一语句,但要求变量已存在且不能是表达式(如$_POST['id'] + 1不合法) - 批量插入多行时,别在循环里反复
prepare(),应复用同一个PDOStatement对象,只换bindValue()值
最常被忽略的点:预处理只保“值”的安全,不保“结构”。哪怕你 99% 的参数都进了占位符,只要漏掉一个表名或排序字段的白名单校验,整个查询就回到裸奔状态。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











