必须关闭PDO模拟预处理并正确设置字符集:初始化时设PDO::ATTR_EMULATE_PREPARES=>false和DSN中指定charset=utf8mb4,否则仍可能遭受SQL注入。

必须关闭PDO模拟预处理
PDO默认开启PDO::ATTR_EMULATE_PREPARES,此时prepare()只是PHP在客户端做字符串替换,跟拼接SQL没区别,完全防不住注入。你得在创建PDO实例时显式关掉它:
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_EMULATE_PREPARES => false]);- 如果连接已建立,用
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false)补救,但不如初始化时设好可靠 - 检查是否生效:执行
$pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES),返回false才算成功
MySQL 5.1.17+才支持原生预处理,老版本即使设false也会自动回退——别跳过版本确认。
字符集必须在DSN里声明
宽字节注入能绕过预处理,根源是客户端与服务端字符集不一致。比如DSN写成"mysql:host=localhost;dbname=test",没带charset,PHP可能用latin1连接,而表是utf8mb4,攻击者用%df%27就能触发截断绕过。
- DSN中强制指定:
"mysql:host=localhost;dbname=test;charset=utf8mb4" - 不要用
$pdo->exec("SET NAMES utf8mb4")替代,它晚于连接建立,存在时间窗口 - 验证是否生效:
$pdo->query("SELECT @@character_set_client, @@collation_connection")->fetch(),两个值都应为utf8mb4
占位符类型和绑定方式要匹配实际数据
命名占位符(如:name)和问号占位符(?)安全等级一样,但绑定逻辑不同,混用容易出错。
立即学习“PHP免费学习笔记(深入)”;
- 用命名占位符时,
execute()必须传关联数组:$stmt->execute(['name' => $input, 'status' => $status]) - 用问号占位符时,
execute()必须传索引数组,顺序不能错:$stmt->execute([$input, $status]) - 对整数、布尔等类型,建议显式转换:
(int)$_GET['id']或(bool)$_POST['active'],避免bindValue()时类型推断错误 - LIKE子句的通配符必须在PHP层加:
$search = "%{$_POST['q']}%"; $stmt->execute([$search]),不能写成WHERE name LIKE :q%
错误模式必须设为异常
默认PDO::ERRMODE_SILENT会让错误静默失败,你根本不知道预处理是否真的执行了,更没法捕获连接中断、权限不足等底层问题。
- 初始化时加上:
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - 这样
prepare()失败会抛PDOException,execute()失败也能被捕获,而不是返回false让你手动判断 - 别在生产环境用
PDO::ERRMODE_WARNING,错误信息可能泄露数据库结构到前端
真正起作用的是“服务端解析SQL模板 + 客户端单独传参”这个物理隔离机制,不是函数名本身。漏掉charset、开着模拟预处理、或者用mysql_*()这类废弃函数,前面所有步骤都白搭。



















