滚动新闻 →
SaaS 数据更新出现覆盖问题,版本号字段应该怎样设计 中国通缉犯洗钱超5亿 藏身泰国近4年终落网 埃及苏丹边界紧张 空袭金矿酿平民死伤 台风蔷琵恐转强台 不排除通过台湾 笔记本电脑按电源键后立即关机,电池和主板供电如何判断 Windows 11 文件排序方式怎么选择?按名称、日期和大小管理文件 印尼霾害飘至雅加达上空 触发东盟南部区域最高警戒 SQL 注入是怎么产生的?PHP 开发中使用 PDO 预处理语句避免风险 从苏轼看古代文人的生活与精神世界 台积电在熊本建3奈米厂 日本批准补助资格 红枣与中国传统饮食文化 书法中的留白为什么重要 最后期限临近 中共逼超级富豪交现金补税 让孩子学会在安全范围内帮助被欺负的人 为什么相同分辨率的视频画质不同 长白山天池水怪真容揭秘 实为飞鸟? 进化骗局(30) 化学进化坍塌 生物进化自垮 刚果金打击非法采矿 拘留数百中国矿工 疑中共使馆介入 唐太宗为什么能够建立稳定的政治秩序 旧房住久了显得凌乱应该从哪里开始改善 上海徐浦大桥多车相撞 人员伤亡不明 加拿大购买私人游艇有哪些基本流程 明星真正值钱的是片酬还是未来项目议价权 奇妙的缘分!浙江、宁夏各有一对夫妻同名同姓  中共围台军演期间 一艘“幽灵船”现踪日本海域 加拿大报税后发现子女信息填错怎么办 布莱克斯通法律评论为何影响深远 江苏一女子生下龙凤胎后起诉董事长 引热议 美俄勒冈小学枪击案4死 英勇教师扑倒枪手夺枪 蔷琵转中度台风 吴德荣:可能直扑台湾 加拿大家庭如何判断自己是否承担得起一套房子 为什么现金流对家庭财富管理如此重要 俄州枪案致4死 英雄父亲果断行动击毙枪手 超市自助结账藏陷阱 五大错误让你多花钱 纽约限制移民执法再升级 市长吁私人停车场拒绝ICE 美国购买私人游艇有哪些基本流程 俄罗斯媒体如何解读川普批评泽连斯基 乌克兰在大国交易中面临什么处境 演员为什么不能只看一次电影的片酬 川普对泽连斯基失去耐心 俄乌和平再添变数 美国八州联合起诉OpenAI 控其涉危害青少年

SQL 注入是怎么产生的?PHP 开发中使用 PDO 预处理语句避免风险

发布时间: 2026-10-17 03:00:02    最后更新: 2026-10-17 03:58:04    阅读:16  约13 分钟阅读     

SQL 注入是怎么产生的?PHP 开发中使用 PDO 预处理语句避免风险
图片说明:示意图   图片来源:Public Domain(公有领域)
SQL 注入并不是什么神秘的黑客技巧,它通常来自一个非常普通的编程错误:开发者把本来应该作为“数据”的用户输入,直接拼接进了 SQL 语句,使数据库无法再区分哪些内容属于 SQL 指令,哪些内容只是用户提交的字符串。一旦两者的边界被破坏,攻击者就可能改变原本的查询逻辑,进一步造成未授权数据读取、数据修改、删除,甚至在特定数据库配置和权限条件下扩大到更严重的系统层面风险。

在 PHP 应用中,PDO(PHP Data Objects)提供的预处理语句是防御 SQL 注入最重要的基础机制之一。不过,真正需要理解的并不是“把字符串过滤一下”,而是参数化查询的核心原则:SQL 的结构和外部输入的数据必须保持分离。

例如,一个典型的错误写法可能是:

$username = $_POST['username'];
$password = $_POST['password'];

$sql = "SELECT id FROM users
WHERE username = '$username'
AND password = '$password'";

$stmt = $pdo->query($sql);

这里的问题并不是 $username 本身“危险”,而是它被直接放进了 SQL 代码中。假设攻击者提交的内容包含引号、逻辑运算符或者 SQL 注释符号,就可能改变原始 SQL 的语法结构。

因此,SQL 注入的本质可以理解成一句话:数据进入了代码的位置。

真正正确的做法,是让 SQL 语句先确定结构,再把外部数据作为参数传递:

$sql = "SELECT id FROM users
WHERE username = ?
AND password = ?";

$stmt = $pdo->prepare($sql);
$stmt->execute([$username, $password]);

$user = $stmt->fetch(PDO::FETCH_ASSOC);

此时 $username 和 $password 是参数,而不是 SQL 语句的一部分。用户输入即使包含引号、SQL 关键字或者其他特殊字符,也不会因此改变预先定义好的 SQL 结构。

这里有一个经常被误解的技术细节:预处理语句并不是简单地先生成一条完整 SQL 字符串,然后把占位符“替换”成经过转义的文本。具体实现取决于 PDO 驱动以及是否使用模拟预处理,但从应用开发角度,正确理解应该是 SQL 结构和参数值被分开处理。使用真正的数据库端 prepared statement 时,数据库可以分别处理语句和参数,从根本上避免普通字符串拼接产生的语法注入。

PDO 支持位置占位符和命名占位符。例如:

$sql = "SELECT id, email
FROM users
WHERE username = :username";

$stmt = $pdo->prepare($sql);
$stmt->execute([
':username' => $username
]);

命名参数在复杂查询中通常更容易阅读。不过需要注意,PDO 参数主要用于表示“值”,而不是 SQL 语法本身。

例如下面这种写法是错误思路:

$orderBy = $_GET['sort'];

$sql = "SELECT * FROM news ORDER BY :sort";

开发者可能以为 :sort 会变成一个字段名,但参数绑定解决的是数据值,不是任意 SQL 标识符。数据库字段名、表名、ASC、DESC 等 SQL 结构通常不能通过普通参数占位符直接完成。

如果排序字段来自用户输入,应该使用白名单映射:

$sortMap = [
'date' => 'created_at',
'views' => 'views',
'title' => 'title'
];

$sort = $_GET['sort'] ?? 'date';

$orderBy = $sortMap[$sort] ?? 'created_at';

$sql = "SELECT *
FROM news
ORDER BY $orderBy DESC";

$stmt = $pdo->prepare($sql);
$stmt->execute();

这里 $orderBy 虽然最终进入了 SQL 字符串,但它不是直接使用用户提交的原始内容,而是只能从开发者预先定义的固定字段中选择。这就是动态 SQL 中非常重要的原则:能参数化的值使用参数,不能参数化的 SQL 结构使用严格白名单。

数据库连接本身也应该进行合理配置。例如 MySQL 项目通常可以这样建立 PDO 连接:

$pdo = new PDO(
'mysql:host=localhost;dbname=test;charset=utf8mb4',
'user',
'password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false
]
);

PDO::ATTR_EMULATE_PREPARES 可以控制 PDO 是否使用模拟预处理。对于 MySQL 应用,明确设置为 false 可以让开发者使用原生 prepared statement 路径。不过不能把“关闭模拟预处理”理解成 SQL 注入的唯一防线。真正关键的仍然是不要把外部输入直接拼进 SQL;数据库账户权限、输入验证和应用架构同样重要。

另外,execute() 参数默认并不意味着所有数据都按照开发者想象的数据库类型绑定。需要明确类型时,可以使用 bindValue():

$stmt = $pdo->prepare(
"SELECT * FROM news WHERE id = :id"
);

$stmt->bindValue(
':id',
$id,
PDO::PARAM_INT
);

$stmt->execute();

对于整数 ID、布尔值等参数,在应用层先进行类型验证,再使用正确的 PDO 参数类型,可以让代码意图更加明确。不过类型绑定本身也不是 SQL 注入防护的替代品。

批量操作则是另一个经常写错的地方。例如:

$sql = "INSERT INTO logs (message) VALUES (?, ?, ?)";

这个 SQL 本身就是错误的,因为只有一个 message 列,却提供了三个 VALUES 参数。如果要一次插入三条记录,可以重复执行同一个 prepared statement:

$stmt = $pdo->prepare(
"INSERT INTO logs (message) VALUES (?)"
);

foreach ($messages as $message) {
$stmt->execute([$message]);
}

如果希望一次提交多行,也可以明确建立多个值组:

$sql = "INSERT INTO logs (message)
VALUES (?), (?), (?)";

$stmt = $pdo->prepare($sql);
$stmt->execute([
$message1,
$message2,
$message3
]);

实际项目中,如果数据量较大,还需要考虑事务、批量大小、索引以及数据库写入压力,而不能把“使用 prepared statement”理解成完整的批处理优化方案。

还有一个非常重要的问题是密码。数据库登录验证不应该采用明文密码,也不应该简单地把用户输入的密码与数据库中的明文字符串进行比较。PHP 应使用 password_hash() 保存密码哈希,并通过 password_verify() 验证密码:

$stmt = $pdo->prepare(
"SELECT id, password_hash
FROM users
WHERE username = ?"
);

$stmt->execute([$username]);

$user = $stmt->fetch(PDO::FETCH_ASSOC);

if ($user && password_verify($password, $user['password_hash'])) {
// 登录成功
}

这样,SQL 注入防护和密码安全就分别得到处理。PDO 负责数据库查询参数的安全边界,密码哈希机制负责凭据存储安全,两者解决的是不同问题。

输入验证同样不能被忽略。比如新闻系统的 id 应该是整数,分页参数应该限制合理范围,邮箱应符合预期格式,状态字段应该只能使用规定值。白名单验证能够减少异常输入和业务逻辑攻击面,但它不能取代 prepared statement。即使一个参数看起来“只能是数字”,数据库查询仍然应该采用参数化方式。

同样,htmlspecialchars() 也不能用来防止 SQL 注入。它主要用于 HTML 输出场景,例如:

echo htmlspecialchars(
$title,
ENT_QUOTES,
'UTF-8'
);

它解决的是输出到 HTML 时的字符编码问题,与 SQL 查询参数化属于完全不同的安全边界。一个值进入 SQL、HTML、JavaScript、URL 或 Shell 时,面对的是不同的解释器,因此需要使用针对目标上下文的安全机制。

错误信息处理也属于数据库安全的一部分。生产环境不应该直接把完整的 SQL 异常、数据库主机信息、表名、字段名和堆栈输出给浏览器。开发环境可以保留详细日志,但生产环境应该记录到受保护的日志系统,同时向用户返回有限的错误信息。

最后还有一道经常被忽视的防线:数据库账户权限。

假设一个网站只需要读取新闻,那么它的数据库账户没有必要拥有删除数据库、创建用户或者修改其他数据库的权限。即使应用层出现严重漏洞,最小权限原则也可以限制攻击者能够造成的影响范围。对于需要写入数据的后台系统,也应该按照实际业务拆分权限,而不是所有 PHP 页面都使用一个拥有全部权限的数据库超级账户。

因此,一个真正可靠的 PHP 数据库访问层通常不是依靠某一个“防注入函数”,而是形成完整的边界:外部输入先经过业务层验证,SQL 中的值使用 PDO prepared statement,动态表名和字段名采用严格白名单,密码使用专门的密码哈希机制,生产环境隐藏数据库内部错误,数据库账户遵循最小权限原则,同时对关键操作使用事务和审计机制。

SQL 注入之所以能够长期成为 Web 安全中的经典漏洞,是因为它暴露的是软件设计中一个非常基础的问题:代码和数据的边界没有被正确建立。PDO 预处理语句真正重要的地方,也正在这里。开发者不需要猜测某个字符“够不够安全”,而是应该从设计层面让用户输入永远停留在数据的位置。对于 PHP 项目来说,这比给字符串不断增加各种转义函数可靠得多,也比寄希望于攻击者“不输入奇怪字符”现实得多。

喜欢这篇报道?

使用下面的功能,方便以后继续阅读和分享 MNewsTV

设为 Google 新闻首选来源 让 Google 新闻优先显示 MNewsTV 的最新报道 ›
★ 我的收藏 查看已经收藏的文章
关于文章收藏 收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。 删除收藏请进入「我的收藏」进行管理。
分享这篇报道

捐助(Paypal): https://www.paypal.me/observeccp
订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP