PHP 连接 MySQL 的正确姿势:PDO 预处理为什么能防 SQL 注入
PHP 连接 MySQL 防 SQL 注入的正确做法是使用 PDO 预处理并一律通过占位符传值,连接时关闭 ATTR_EMULATE_PREPARES、开启异常模式并设置 utf8mb4;因为预处理先把 SQL 模板交给数据库编译,参数经独立通道传输、不参与 SQL 解析,所以用户输入只能作为值而无法变成代码,而拼接字符串和仅靠转义都不可靠。
PDO 预处理之所以能防 SQL 注入,根本原因只有一个:它把「SQL 语句的结构」和「用户传入的数据」彻底分开了——先用占位符把语句模板发给数据库编译好,参数再通过独立通道单独传输,数据从头到尾不参与 SQL 解析,所以无论用户输入什么,都只能被当作「值」,永远变不成「代码」。
拼接字符串的写法,为什么一定会被注入
结论:只要用户输入参与拼接 SQL 字符串,注入就是时间问题,字符转义救不了你。
看这段最常见的错误写法:
$sql = "SELECT * FROM users WHERE email = '$email' AND status = 1";
$row = $pdo->query($sql)->fetch();
攻击者把 $email 填成 ' OR 1=1 -- ,拼接后语句变成 WHERE email = '' OR 1=1 -- ' AND status = 1,-- 后面全部被注释掉,登录校验直接绕过。这类字符串在 MySQL 里称为「被解析成 SQL 语法的一部分」,危险就在这。
有人会说「我用 mysqli_real_escape_string 转义过」。转义能挡住大部分场景,但它依赖当前连接的字符集是否正确(历史上宽字节注入就出在这),而且一旦忘了转义某处、或者转义后又做了一次 urldecode,防线就破了。安全逻辑不该建立在「每次都记得转义」上。
PDO 的正确姿势:四条连接参数 + 占位符
结论:连接时关掉模拟预处理、开启异常模式,查询时一律用占位符传值,这是 PHP 访问 MySQL 的推荐基线。
$dsn = 'mysql:host=127.0.0.1;dbname=forum;charset=utf8mb4';
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 出错抛异常,别静默
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 关闭模拟预处理,走 MySQL 原生预处理
PDO::ATTR_STRINGIFY_FETCHES => false,
]);
ATTR_EMULATE_PREPARES 这一条尤其关键:PDO_MySQL 默认是开启模拟预处理的,此时 PDO 自己在前端做转义和字符串拼接,等于又回到了「靠转义吃饭」的老路。设为 false 后,语句模板发给 MySQL 编译,参数经二进制协议单独传输,注入面被彻底切断。charset=utf8mb4 写在 DSN 里也比事后 SET NAMES 更可靠。
查询写法:
$stmt = $pdo->prepare('SELECT id, username FROM users WHERE email = ? AND status = ?');
$stmt->execute([$email, 1]);
$user = $stmt->fetch();
命名占位符同样可用:WHERE email = :email,执行时传 [':email' => $email],可读性更好,适合参数多的场景。顺便说一句,PHP 8.0 起错误模式默认已经是异常,但显式写出来更稳妥,老版本默认是静默失败,调试时会很痛苦。
占位符只能给「值」,这些位置必须走白名单
结论:占位符不能用在表名、列名、ORDER BY、LIMIT 等 SQL 结构位置上,这些地方只能用白名单映射。
这是最容易被误解的一点:ORDER BY ? 或 FROM ? 根本不会按你期望工作,因为它们是标识符/关键字,不是值。正确做法是白名单:
$allow = ['created_at', 'replies', 'views'];
$order = in_array($order, $allow, true) ? $order : 'created_at';
$sort = $sort === 'asc' ? 'ASC' : 'DESC';
$limit = max(1, min(50, (int)$limit)); // 强转成整数,不可能是任意字符串
$sql = "SELECT id, title FROM posts ORDER BY {$order} {$sort} LIMIT {$limit}";
IN 列表则用占位符数量动态生成,元素先强转整数:
$ids = array_map('intval', $ids);
$in = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT id, title FROM posts WHERE id IN ($in)");
$stmt->execute($ids);
三个高频踩坑
结论:同名命名占位符、LIKE 通配符、二次注入,是预处理用对了也会翻车的三个点。
第一,同名命名占位符在关闭模拟预处理后会报错。WHERE a = :kw OR b = :kw 在原生预处理下会抛 HY093,需要写成两个不同名字(:kw1、:kw2)。开启模拟时能跑,关掉后报错,很多人升级配置后才发现。
第二,LIKE 的通配符要处理。用户搜索词里带 % 或 _ 会被当通配符,虽然不构成注入,但会拖慢查询、返回脏结果,建议对 %、_、\ 做转义后再包上 %...%。
第三,二次注入不在预处理的保护范围内。数据用预处理安全存进库,之后又被取出直接拼进另一条 SQL,照样中招。防御靠的是「所有 SQL 一律走预处理」这个纪律,而不是某几处写了预处理。
预处理管不到的边界
结论:预处理只管「数据不被当代码执行」,XSS 输出转义、数据库账号最小权限、二次注入,都需要另外一层防线。
SQL 注入和 XSS 是两码事:PDO 帮你在入库时不炸数据库,但把用户昵称原样 echo 到页面照样是 XSS,输出必须做 HTML 转义。另外建议给应用配一个最小权限数据库账号,只授 SELECT/INSERT/UPDATE/DELETE,不给 DROP、FILE、INTO OUTFILE——这样即使某处漏了,攻击者也拿不到拖库或写文件的能力。
环境层面不用担心:以 Clara BBS 这类社区系统要求的 PHP 7.4–8.5 + MySQL 5.7+ 为例,PDO 及 pdo_mysql 扩展属于 PHP 自带且默认启用的组件,不需要额外安装,直接在代码里 new PDO(...) 就能用。
一句话收尾:把 ATTR_EMULATE_PREPARES 关掉、连接时 charset=utf8mb4、值一律走占位符、结构位置一律走白名单,这四条做到,SQL 注入这个坑基本就填平了;剩下的 XSS、越权、二次注入,是另外的账要另算。
转载请注明出处,版权归原作者所有。
正式会员
认证极客






