Skip to content

16.1 防 SQL 注入

SQL 注入攻击原理

SQL 注入是一种常见的网络攻击方式,攻击者通过在用户输入中插入恶意 SQL 代码,来操纵数据库查询,从而获取敏感信息或执行未授权操作。

攻击示例

假设有一个登录表单,代码如下:

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

$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);

如果攻击者输入以下内容作为用户名:

admin' --

那么生成的 SQL 查询将变成:

sql
SELECT * FROM users WHERE username = 'admin' --' AND password = '任意值'

由于 -- 是 SQL 注释符号,后面的密码验证部分会被注释掉,攻击者可以无需密码登录系统。

防止 SQL 注入的方法

1. 使用预处理语句

预处理语句是防止 SQL 注入的最佳方法,它将 SQL 语句的结构与数据分开处理。

使用 mysqli 预处理语句

php
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
$result = $stmt->get_result();

使用 PDO 预处理语句

php
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);
$result = $stmt->fetchAll();

2. 输入验证和过滤

对用户输入进行验证和过滤,确保输入符合预期格式。

php
// 验证用户名只包含字母、数字和下划线
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
    die("用户名格式不正确");
}

// 验证密码长度
if (strlen($password) < 6) {
    die("密码长度至少为 6 位");
}

3. 转义特殊字符

如果无法使用预处理语句,可以使用 mysqli_real_escape_string() 函数来转义特殊字符。

php
$username = mysqli_real_escape_string($conn, $_POST['username']);
$password = mysqli_real_escape_string($conn, $_POST['password']);

$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);

4. 限制数据库用户权限

为应用程序创建专用的数据库用户,并限制其权限,只授予必要的权限。

sql
-- 创建只读用户
CREATE USER 'app_read'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON database.* TO 'app_read'@'localhost';

-- 创建读写用户
CREATE USER 'app_write'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE, DELETE ON database.* TO 'app_write'@'localhost';

5. 使用参数化查询

参数化查询是一种将 SQL 语句与参数分离的方法,可以有效防止 SQL 注入。

php
// 使用 mysqli 参数化查询
$stmt = $conn->prepare("INSERT INTO users (username, email) VALUES (?, ?)");
$stmt->bind_param("ss", $username, $email);
$stmt->execute();

// 使用 PDO 参数化查询
$stmt = $pdo->prepare("INSERT INTO users (username, email) VALUES (:username, :email)");
$stmt->execute([':username' => $username, ':email' => $email]);

6. 避免使用动态 SQL

尽量避免使用动态构建的 SQL 语句,特别是当 SQL 语句中包含用户输入时。

php
// 不安全的动态 SQL
$sql = "SELECT * FROM users WHERE " . $_POST['condition'];

// 安全的做法
$allowed_conditions = ['id', 'username', 'email'];
$condition = in_array($_POST['field'], $allowed_conditions) ? $_POST['field'] : 'id';
$sql = "SELECT * FROM users WHERE $condition = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $_POST['value']);
$stmt->execute();

7. 定期更新和补丁

保持数据库系统和应用程序框架的更新,及时安装安全补丁。

8. 使用 ORM 框架

使用 ORM(对象关系映射)框架,如 Laravel 的 Eloquent、Doctrine 等,这些框架会自动处理 SQL 注入防护。

php
// 使用 Laravel Eloquent
$user = User::where('username', $username)->where('password', $password)->first();

// 使用 Doctrine
$user = $em->getRepository(User::class)->findOneBy(['username' => $username, 'password' => $password]);

实战演练

场景:用户登录系统

不安全的实现

php
<?php
$username = $_POST['username'];
$password = $_POST['password'];

$conn = mysqli_connect('localhost', 'root', '', 'test');

$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);

if (mysqli_num_rows($result) > 0) {
    echo "登录成功";
} else {
    echo "用户名或密码错误";
}
?>

安全的实现

php
<?php
$username = $_POST['username'];
$password = $_POST['password'];

$conn = mysqli_connect('localhost', 'root', '', 'test');

// 使用预处理语句
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
$result = $stmt->get_result();

if (mysqli_num_rows($result) > 0) {
    echo "登录成功";
} else {
    echo "用户名或密码错误";
}
?>

场景:搜索功能

不安全的实现

php
<?php
$keyword = $_GET['keyword'];

$conn = mysqli_connect('localhost', 'root', '', 'test');

$sql = "SELECT * FROM products WHERE name LIKE '%$keyword%'";
$result = mysqli_query($conn, $sql);
?>

安全的实现

php
<?php
$keyword = $_GET['keyword'];

$conn = mysqli_connect('localhost', 'root', '', 'test');

// 使用预处理语句
$stmt = $conn->prepare("SELECT * FROM products WHERE name LIKE ?");
$keyword = "%$keyword%";
$stmt->bind_param("s", $keyword);
$stmt->execute();
$result = $stmt->get_result();
?>

总结

SQL 注入是一种严重的安全漏洞,可能导致数据库被攻击、敏感信息泄露等问题。通过使用预处理语句、输入验证、转义特殊字符、限制数据库用户权限等方法,可以有效防止 SQL 注入攻击。

在开发过程中,应始终将安全放在首位,遵循安全编码实践,定期进行安全审计和测试,确保应用程序的安全性。

© 2026 编程马·菜鸟教程 版权所有