本文目录导读:

在PHP项目中防止SQL注入,最有效、最推荐的方法是使用预处理语句(Prepared Statements),它可以确保SQL语句的结构不会被用户输入的数据改变。
以下是几种核心的防护方法,按推荐程度从高到低排列:
使用 PDO 或 MySQLi 的预处理语句(最佳实践)
预处理语句将SQL模板与数据分开,即使数据中包含恶意SQL代码,它也只会被当作字符串处理,而不会改变SQL的逻辑。
方法 A:PDO(推荐,支持多种数据库)
<?php
// 1. 创建 PDO 连接(设置错误模式和模拟预处理关闭)
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4';
$pdo = new PDO($dsn, 'username', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 关闭模拟预处理,使用真实的数据库预处理
]);
// 2. 准备SQL语句(使用 ? 占位符 或 :name 命名占位符)
$sql = "SELECT * FROM users WHERE username = :username AND password = :password";
$stmt = $pdo->prepare($sql);
// 3. 绑定参数并执行
$stmt->execute([
':username' => $_POST['username'],
':password' => hash('sha256', $_POST['password']), // 示例:密码不要明文存储
]);
// 4. 获取结果
$user = $stmt->fetch();
?>
关键点:
PDO::ATTR_EMULATE_PREPARES => false:让 PHP 使用数据库真实的预处理功能,而不是模拟,这能更彻底地防止注入。- 使用 或
name占位符:不要手动拼接变量。
方法 B:MySQLi(面向对象风格)
<?php
// 1. 创建 MySQLi 连接
$conn = new mysqli('localhost', 'username', 'password', 'test');
// 检查连接
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
// 2. 准备SQL语句
$sql = "SELECT * FROM users WHERE username = ?";
$stmt = $conn->prepare($sql);
// 3. 绑定参数(s = 字符串,i = 整数,d = 双精度,b = 二进制)
$username = $_POST['username'];
$stmt->bind_param("s", $username);
// 4. 执行
$stmt->execute();
// 5. 获取结果
$result = $stmt->get_result();
$user = $result->fetch_assoc();
// 6. 关闭连接
$stmt->close();
$conn->close();
?>
永远不要手动转义(贬义做法)
不推荐的做法是使用 mysqli_real_escape_string()。
- 原因:它容易出错,需要正确设置数据库连接编码(如
set_charset('utf8')),且容易忘记处理某些字符,一旦忘记使用,就不安全。 - 放弃
addslashes()和mysqli_real_escape_string(),改用预处理语句。
输入验证与白名单(补充措施)
对于数值类型或固定选项,验证数据类型可以增加一层防护。
// 示例:确保 ID 是整数
$id = (int)$_GET['id']; // 强制转换为整数,去掉所有非数字字符
// 示例:确保排序字段在白名单中
$allowedSort = ['name', 'date', 'id'];
$sort = $_GET['sort'] ?? 'id';
if (!in_array($sort, $allowedSort)) {
$sort = 'id'; // 默认值,防止 SQL 注入
}
注意:验证不能替代预处理语句,只能作为额外保险。
防范 ORDER BY、LIMIT 等特殊场景
有些SQL子句(如 ORDER BY、LIMIT、表名、列名)不能使用预处理语句绑定参数,此时需要严格的白名单验证:
$direction = strtoupper($_GET['dir']); // ASC 或 DESC
if (!in_array($direction, ['ASC', 'DESC'])) {
$direction = 'ASC';
}
$sql = "SELECT * FROM products ORDER BY price $direction";
其他安全措施(防御纵深)
- 最小权限原则:数据库用户只授予必要的权限,应用用户通常只要
SELECT、INSERT、UPDATE、DELETE,不需要DROP、CREATE。 - 隐藏错误信息:生产环境关闭
display_errors,避免 SQL 报错信息被攻击者利用。 - 使用 ORM:成熟的 ORM(如 Doctrine、Eloquent)多数情况下会自动使用预处理语句,但如果使用
raw查询方法(如DB::raw()),风险仍在,仍需手动处理。
关键原则
| 做法 | 是否推荐 | 说明 |
|---|---|---|
| PDO 预处理语句 | ✅ 强烈推荐 | 最佳实践,兼容多数据库 |
| MySQLi 预处理语句 | ✅ 推荐 | 只用于 MySQL |
mysqli_real_escape_string() |
❌ 不推荐 | 容易遗漏,有编码风险 |
addslashes() |
❌ 禁止 | 完全不可靠 |
| 输入验证(类型、白名单) | ✅ 补充使用 | 不能替代预处理,但可以增加安全性 |
一句话总结:凡是SQL查询中出现用户输入的地方(包括GET参数、POST数据、Cookie、文件上传名等),都应当使用预处理语句插入数据。