必须显式设置pdo::attr_emulate_prepares => false,否则prepare()退化为php层字符串拼接,宽字节或旧mysql下易被绕过;需配合占位符绑定、白名单校验标识符及utf8mb4字符集。

只要 PDO 连接没设 PDO::ATTR_EMULATE_PREPARES => false,哪怕你写了 prepare() + execute(),也大概率白防——PHP 8 默认仍开启模拟预处理,攻击者在宽字节或旧 MySQL 环境下能绕过。
必须关闭模拟预处理:PDO::ATTR_EMULATE_PREPARES => false
这是整个防护链条的起点。PHP 8 默认仍为 true,意味着 prepare() 只是 PHP 自己做字符串替换,不是交由 MySQL 服务端真正编译语句模板。
- 显式设置:创建 PDO 实例时必须传入该选项,
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); - 验证是否生效:执行
$pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES),返回false才算成功 - 失败表现:若 MySQL 不支持服务端预处理(如极老版本),
prepare()会返回false,不能直接链式调用bindValue(),否则触发致命错误 - 连带要求:DSN 中必须显式指定字符集,如
charset=utf8mb4,否则宽字节注入风险仍在
绑定方式错一个环节,预处理就失效
prepare() 本身不防注入,只有配合参数绑定才起作用。常见错误是“写了 prepare 就以为安全了”,实际只是空壳。
- ❌ 错误写法:
$pdo->prepare("SELECT * FROM users WHERE name = '{$_POST['name']}'")—— 占位符都没写,纯拼接 - ✅ 正确链条:含占位符的 SQL →
prepare()→bindValue()/bindParam()/execute()三者缺一不可 - 命名参数用
execute([':name' => $val])时,键名必须严格匹配(冒号、大小写、空格都不能差) - 位置参数用
bindValue(1, $val, PDO::PARAM_STR),索引从 1 开始,不是数组下标 0
表名、字段名、ORDER BY 无法参数化,白名单是唯一解
SQL 语法规定,这些属于标识符(identifier),不是值(value),占位符根本不起作用。任何试图用 addslashes()、mysqli_real_escape_string() 或正则过滤来“转义字段名”的做法都无效且危险。
- 排序字段校验示例:
$allowed_sorts = ['id', 'title', 'created_at']; $sort = $_GET['sort'] ?? 'id'; if (!in_array($sort, $allowed_sorts)) { throw new InvalidArgumentException('Invalid sort field'); } - 排序方向强制大写并限定:
$order = strtoupper($_GET['order'] ?? 'ASC'); if (!in_array($order, ['ASC', 'DESC'])) { ... } - 动态表名建议映射:
$table_map = ['user' => 'users', 'post' => 'blog_posts']; $table = $table_map[$_GET['type']] ?? 'users'; - 别用
filter_var($input, FILTER_SANITIZE_STRING)处理字段名——它删 HTML 标签,不校验语义合法性
PHP 8 的 strict_types=1 是隐形加固层
它不直接防注入,但能提前暴露类型污染问题,避免“看似安全实则漏防”的静默失败。
- 函数参数声明为
int,用户传id[]=1(数组)会直接抛TypeError,而不是被(int)强转成 0 导致逻辑错乱 - 配合
PDO::PARAM_INT绑定时,若变量实际是字符串,PDO 不会自动转换,而是报错或截断,让你立刻发现问题 - 数字型输入建议叠加校验:
filter_var($_GET['id'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]),比单纯强转更严谨 - LIKE 查询中通配符必须在 PHP 层加:
$search = '%' . $_POST['keyword'] . '%'; $stmt->execute([$search]);,不能写成LIKE :keyword%
最易被忽略的点是:预处理只保“值”,不保“结构”。哪怕你每个 WHERE 条件都用了占位符,只要有一处拼了 $table 或 $field,整条语句就退化为不安全状态——这种漏洞不会报错,但攻击面完全打开。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











