Skip to content

MySQL - SQL 注入

SQL 注入(SQL Injection)是一种可能破坏数据库的代码注入技术。它是最常见的网络攻击技术之一。SQL 注入通过网页输入,在 SQL 语句中插入恶意代码。当应用程序在构建 SQL 查询时,没有正确验证或清理用户提供的数据时,就会发生 SQL 注入。

设想一个简单的登录表单,应用程序通过将用户输入直接拼接成 SQL 字符串来构建查询:

// 这是一个脆弱(易受攻击)代码的示例。请勿使用。
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username = '" . $username . "' AND password = '" . $password . "';";

合法用户会输入他们的用户名和密码。但攻击者可以在 username 字段中输入以下内容:

' OR '1'='1

应用程序随后会构建这个恶意的 SQL 查询:

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'some_password';

因为条件 '1'='1' 始终为真,所以 WHERE 子句对每一行都评估为真,查询会返回表中的所有用户。攻击者无需有效密码就成功绕过了登录。

防止 SQL 注入最有效的方法是始终使用预处理语句(Prepared Statements,也称为参数化查询)。这种技术将 SQL 命令结构与数据分离。

其工作原理如下:

  1. 准备(Prepare): SQL 查询与占位符(例如 ? 或 :name)一起发送到数据库服务器,而不是用户数据。数据库解析并编译此查询模板。
  2. 绑定(Bind): 应用程序随后单独将用户提供的数据发送到数据库。
  3. 执行(Execute): 数据库将编译好的模板与绑定的数据结合起来并执行查询。关键在于,数据被严格地视为数据,不能被解释为 SQL 命令的一部分。

像 mysql_real_escape_string() 这样的老旧方法已经被弃用且不安全。它们很容易实现错误,并且无法提供与预处理语句相同级别的保护。

示例:使用 PHP (MySQLi) 实现安全登录

Section titled “示例:使用 PHP (MySQLi) 实现安全登录”

下面是使用 PHP 的 MySQLi 扩展和预处理语句实现的登录检查安全示例。

<?php
// 数据库连接详情
$host = 'localhost';
$db_user = 'your_db_user';
$db_pass = 'your_db_password';
$db_name = 'your_db_name';
$conn = new mysqli($host, $db_user, $db_pass, $db_name);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// 从 POST 请求获取用户名
$username = $_POST['username'];
// 1. 使用占位符准备语句
$stmt = $conn->prepare("SELECT user_id, password_hash FROM users WHERE username = ?");
if ($stmt === false) {
die("Prepare failed: " . $conn->error);
}
// 2. 将用户输入绑定到占位符
$stmt->bind_param("s", $username); // "s" 表示参数是字符串类型
// 3. 执行查询
$stmt->execute();
// 获取结果
$result = $stmt->get_result();
if ($result->num_rows === 1) {
$user = $result->fetch_assoc();
// 使用 password_verify() 验证密码
if (password_verify($_POST['password'], $user['password_hash'])) {
echo "Login successful!";
// 继续创建会话等操作
} else {
echo "Invalid username or password.";
}
} else {
echo "Invalid username or password.";
}
$stmt->close();
$conn->close();
?>
import mysql.connector
from mysql.connector import errorcode
def get_user_by_username(username):
try:
# 连接详情应安全存储,而不是硬编码
cnx = mysql.connector.connect(
user='your_user',
password='your_password',
host='127.0.0.1',
database='your_db'
)
cursor = cnx.cursor(dictionary=True)
query = "SELECT user_id, username FROM users WHERE username = %s"
# 当您将参数作为元组传递时,连接器会处理数据清理
cursor.execute(query, (username,))
user = cursor.fetchone()
if user:
print(f"Found user: {user}")
else:
print("User not found.")
except mysql.connector.Error as err:
print(f"Something went wrong: {err}")
finally:
if 'cursor' in locals() and cursor:
cursor.close()
if 'cnx' in locals() and cnx.is_connected():
cnx.close()
# 示例用法
get_user_by_username("admin")
# 尝试注入攻击 - 它将安全地找不到名为 "' OR '1'='1'" 的用户
get_user_by_username("' OR '1'='1'")

预处理语句也解决了在 LIKE 子句中使用用户输入的挑战。您只需将通配符(%、_)包含在您绑定的数据中,而不是在 SQL 查询字符串本身中。

// PHP (MySQLi) 搜索功能示例
$searchTerm = $_GET['search'];
// 准备语句
$stmt = $conn->prepare("SELECT product_name FROM products WHERE product_name LIKE ?");
// 向要绑定的变量添加通配符
$likeTerm = "%$searchTerm%";
// 绑定参数
$stmt->bind_param("s", $likeTerm);
$stmt->execute();
// ... 获取并显示结果 ...
  • 始终使用预处理语句: 这是黄金法则,没有例外。
  • 最小权限原则: 使用仅具有所需权限的用户连接到数据库。对于 Web 应用程序的面向公众的部分,此用户不应具有 DROP TABLE 或更改 schema 的权限。
  • 使用 ORM: 对象关系映射器(Object-Relational Mappers,例如 Laravel 中的 Eloquent、Python 中的 SQLAlchemy、Node.js 中的 TypeORM)通常默认使用预处理语句,增加了一层抽象和安全性。
  • 验证和清理输入: 虽然预处理语句是主要防御手段,但您仍应在应用程序级别验证用户输入,以确保其符合预期格式(例如,电子邮件地址看起来像电子邮件)。
  • 永不信任用户输入: 将所有来自客户端的数据(表单、URL 参数、cookie、API 请求)视为潜在恶意,直到另有证明为止。