PDO防SQL注入的核心是隔离SQL结构与用户数据:必须关闭模拟预处理、DSN声明字符集、仅对值用占位符、表名等非值元素白名单校验、配合业务验证与最小权限控制。

用 PDO 防止 SQL 注入,核心不是“用了 prepare 就安全”,而是确保 SQL 结构与用户数据彻底隔离。关键在连接配置、占位符使用规范、以及非值类元素(如表名、排序字段)的严格管控。
必须关闭模拟预处理
MySQL 驱动默认开启模拟预处理(PDO::ATTR_EMULATE_PREPARES => true),此时 PHP 在客户端拼接 SQL 并转义参数,等同于字符串拼接,宽字节、多字节编码等场景仍可绕过。必须在 new PDO() 后立即设置:
- $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
- DSN 中显式声明字符集:mysql:host=localhost;dbname=test;charset=utf8mb4
- PostgreSQL 和 SQLite 默认走真实预处理,设为 false 无副作用;若设后报错 SQLSTATE[HY000]: General error: 2025,说明 MySQL 版本过低(需 5.1.17+)
只对值使用占位符
占位符(? 或 :name)只能代入数据值,不能代入表名、字段名、ORDER BY 字段或 LIMIT 数值。以下写法完全不防注入:
- $table = $_GET['tab']; $stmt = $pdo->prepare("SELECT * FROM {$table} WHERE id = ?"); —— {$table} 是 PHP 字符串拼接,prepare 不处理它
- 攻击者传 tab=users; DROP TABLE users-- 即可执行任意语句
- 正确做法:表名/字段名必须硬编码,或从白名单中校验:in_array($table, ['users', 'posts'])
优先使用命名占位符
命名占位符(如 :email、:status)比问号更安全、易维护,尤其当多个参数重复或顺序复杂时:
立即学习“PHP免费学习笔记(深入)”;
- SQL 中写:WHERE email = :email AND status = :status AND created_at > :from
- execute 时传关联数组:['email' => $e, 'status' => $s, 'from' => $t]
- 避免问号占位符因顺序错位导致绑定失败或逻辑错误
配套必须做的验证与限制
参数化查询只解决语法注入,不替代业务校验和权限控制:
- ID 类整数用 filter_var($id, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]),不用 (int) 强转
- LIKE 查询的通配符必须在 PHP 层添加:$keyword = '%' . trim($_GET['q']) . '%'; $stmt->execute([$keyword]);
- 动态排序字段(如 ORDER BY)必须白名单:in_array($_GET['sort'] ?? '', ['name', 'created_at', 'email'])
- 数据库账号仅授予 SELECT/INSERT/UPDATE/DELETE,禁用 DROP、CREATE、FILE、ALTER 等高危权限



















