⭐ 推荐:社区规则条款 V1.0

PHP 连接 MySQL 的正确姿势:PDO 预处理为什么能防 SQL 注入

不语
不语 正式会员正式会员认证极客认证极客
发布于 2026-08-10 13:39 ·97 浏览 ·0 回复
内容摘要

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、越权、二次注入,是另外的账要另算。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-352.html
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
CLARA轻量论坛系统

全部回复 0

还没有回复,来抢沙发~