PDO预处理防注入必须禁用模拟预处理(ATTR_EMULATE_PREPARES=false),所有用户输入须用占位符绑定,数字类型用PARAM_INT,表名/字段名/排序需白名单校验,字符集设utf8mb4并启用异常模式。

用 PDO::prepare() + bindValue() 或 execute() 绑定参数是防 SQL 注入唯一靠谱的路径。拼接字符串、依赖转义函数、或只靠过滤输入,都不足以构成有效防护。
必须禁用模拟预处理(PDO::ATTR_EMULATE_PREPARES => false)
MySQL 默认开启模拟预处理,即 PDO 在 PHP 层把占位符替换成带引号的字符串再发给数据库——这等于又回到了字符串拼接,攻击者传入 ' OR 1=1 -- 仍可能生效。
- 连接时显式关闭:
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_EMULATE_PREPARES => false]); - 不设这个选项,
prepare()可能退化为字符串替换,尤其在低版本 MySQL 或某些字符集配置下 - 验证是否生效:执行
$pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES),返回false才算成功
所有用户输入都得走占位符,哪怕“看起来安全”
WHERE、INSERT、UPDATE、DELETE 中任何来自 $_GET、$_POST、$_COOKIE 或外部 API 的值,只要进 SQL,就必须用 ? 或 :name 占位。
- ✅ 正确:
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ? AND status = ?');→$stmt->execute([$id, $status]); - ❌ 错误:
"WHERE id = " . (int)$id或"WHERE name LIKE '%" . $search . "%' - 数字类型优先用
PDO::PARAM_INT,避免隐式转换导致绕过,比如传入'1 OR 1=1'被当成整数 1 后截断失效,但仍有风险
表名、字段名、排序方向不能参数化,必须白名单校验
SQL 语法不允许把 ORDER BY ? 或 FROM ? 当作参数绑定——数据库解析阶段就报错。这部分只能靠代码逻辑兜底。
立即学习“PHP免费学习笔记(深入)”;
- 排序字段:
in_array($sort, ['id', 'name', 'created_at'], true),不接受$_GET['sort']直接代入 - 排序方向:
strtoupper($dir) === 'ASC' || strtoupper($dir) === 'DESC',强制大写后比对 - 表名/字段名若需动态,必须映射到固定数组:
$tables = ['users' => 'users', 'logs' => 'admin_logs']; $table = $tables[$input] ?? 'users';
错误模式和字符集要配齐,否则埋雷
静默失败或乱码会掩盖真实问题,比如中文被截断后触发非预期行为,或错误信息泄露数据库结构。
- 启用异常模式:
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,让错误直接抛出,不靠if ($stmt->errorCode())手动查 - 字符集强制设为
utf8mb4:mysql:host=localhost;dbname=test;charset=utf8mb4,避免宽字节注入(如 %df%27 这类绕过) - 不设
charset参数时,PDO 可能用 latin1 连接,导致mb_strlen()校验失效、emoji 存储异常、甚至注入绕过
真正难的不是写对那几行 prepare() 和 execute(),而是确保每一处 SQL 构造都经过同一套规则约束——尤其是那些“只查一条数据”“只是后台管理”的接口,最容易漏掉白名单或忘关模拟预处理。



















