PHP 中防止 SQL 注入的预处理语句实践

SQL 注入长期位居 OWASP Top 10 榜首,是 Web 应用面临的最严重安全威胁之一。PHP 作为 Web 开发的主流语言,其与数据库交互的环节往往是注入攻击的突破口。本文将深入探讨如何通过预处理语句从根本上防止 SQL 注入,并结合常见误区与最佳实践,帮助开发者构建更安全的 PHP 应用。

一、SQL 注入的本质

SQL 注入产生的根本原因在于:用户输入被直接拼接进 SQL 语句,导致数据库将恶意输入解析为 SQL 代码而非普通数据。例如:

$sql = "SELECT * FROM users WHERE username = '$_GET[user]'";

当攻击者传入 ' OR '1'='1 时,查询条件被篡改,可能绕过认证或泄露整张表的数据。

要彻底解决这个问题,核心思路是将 SQL 语句的结构与数据分离——这正是预处理语句(Prepared Statements)的设计初衷。

二、PDO 预处理语句的正确用法

PDO(PHP Data Objects)是 PHP 官方推荐的数据库抽象层,支持多种数据库,且默认提供预处理能力。

2.1 命名占位符

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

$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username AND status = :status');
$stmt->execute([
    ':username' => $_POST['username'],
    ':status'   => 1,
]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

2.2 位置占位符

$stmt = $pdo->prepare('INSERT INTO articles (title, content) VALUES (?, ?)');
$stmt->execute([$title, $content]);

关键点:

  • 占位符只用于数据,不能用于表名、列名、ORDER BY 字段。这些结构性元素必须使用白名单校验。
  • 关闭模拟预处理PDO::ATTR_EMULATE_PREPARES => false 让 MySQL 真正执行服务端预处理,避免某些边缘场景下的注入风险。
  • 设置字符集:连接串中指定 charset=utf8mb4,防止宽字节注入。

三、MySQLi 预处理语句

如果项目使用 MySQLi,同样应优先使用其预处理接口。

$mysqli = new mysqli('localhost', $user, $pass, 'test');
$stmt = $mysqli->prepare('SELECT id, email FROM users WHERE email = ?');
$stmt->bind_param('s', $email);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
    // 处理结果
}
$stmt->close();

bind_param 的第一个参数是类型字符串:s(字符串)、i(整数)、d(浮点)、b(二进制)。类型必须与数据库字段匹配,否则可能引发隐式转换问题。

四、常见误区与错误写法

即便使用了预处理,以下写法依然存在风险:

4.1 拼接表名或字段名

// 错误:order 来自用户输入
$sql = "SELECT * FROM users ORDER BY {$_GET['order']}";

正确做法是白名单映射:

$allowed = ['id', 'created_at', 'username'];
$order = in_array($_GET['order'] ?? '', $allowed, true) ? $_GET['order'] : 'id';
$sql = "SELECT * FROM users ORDER BY $order";

4.2 使用 LIKE 时忘记转义通配符

$stmt = $pdo->prepare('SELECT * FROM posts WHERE title LIKE ?');
$stmt->execute(['%' . str_replace(['%', '_'], ['\%', '\_'], $keyword) . '%']);

虽然占位符能防止注入,但 %_ 作为通配符会改变查询语义,需要额外转义。

4.3 认为预处理能防御一切

预处理只解决 SQL 注入,不能防御 XSS、CSRF、越权访问。例如把用户输入直接输出到 HTML 页面,依然会触发 XSS。输出时应使用 htmlspecialchars($str, ENT_QUOTES, 'UTF-8') 进行转义,并设置正确的 Content-Security-Policy 响应头。

五、从开发流程上加固

仅靠单点技术不足以构建安全体系,建议在流程层面形成约束:

  1. 统一数据访问层:封装数据库操作类,禁止业务代码直接拼接 SQL。
  2. 代码审计与静态扫描:使用 PHPStan、Psalm 或 SonarQube 检测可疑的字符串拼接。
  3. 最小权限原则:数据库账号只授予必要权限,禁止使用 root 连接业务库。
  4. 错误信息不暴露:生产环境关闭 display_errors,避免泄露 SQL 结构。
  5. 日志与监控:记录异常查询,及时发现注入尝试。

六、结语

预处理语句是 PHP 防 SQL 注入的基石,但它的正确使用依赖于开发者对细节的把握:关闭模拟预处理、区分数据与结构、白名单校验动态字段、配合输出转义防御 XSS。只有将安全编码习惯融入日常开发流程,才能真正降低 Web 应用的攻击面。

安全不是一次性的任务,而是持续的实践。从今天起,检查你的项目中是否还有拼接 SQL 的代码,把它们全部替换为预处理语句——这是投入产出比最高的一步。

未经允许不得转载:任鹏个人博客 » PHP 中防止 SQL 注入的预处理语句实践

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏