MySQL - SQL 注入
MySQL - 防止 SQL 注入
Section titled “MySQL - 防止 SQL 注入”SQL 注入(SQL Injection)是一种可能破坏数据库的代码注入技术。它是最常见的网络攻击技术之一。SQL 注入通过网页输入,在 SQL 语句中插入恶意代码。当应用程序在构建 SQL 查询时,没有正确验证或清理用户提供的数据时,就会发生 SQL 注入。
经典 SQL 注入攻击的工作原理
Section titled “经典 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 子句对每一行都评估为真,查询会返回表中的所有用户。攻击者无需有效密码就成功绕过了登录。
现代解决方案:预处理语句
Section titled “现代解决方案:预处理语句”防止 SQL 注入最有效的方法是始终使用预处理语句(Prepared Statements,也称为参数化查询)。这种技术将 SQL 命令结构与数据分离。
其工作原理如下:
- 准备(Prepare): SQL 查询与占位符(例如
?或:name)一起发送到数据库服务器,而不是用户数据。数据库解析并编译此查询模板。 - 绑定(Bind): 应用程序随后单独将用户提供的数据发送到数据库。
- 执行(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();?>示例:使用 Python 安全获取数据
Section titled “示例:使用 Python 安全获取数据”import mysql.connectorfrom 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 子句
Section titled “处理 LIKE 子句”预处理语句也解决了在 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();// ... 获取并显示结果 ...安全最佳实践
Section titled “安全最佳实践”- 始终使用预处理语句: 这是黄金法则,没有例外。
- 最小权限原则: 使用仅具有所需权限的用户连接到数据库。对于 Web 应用程序的面向公众的部分,此用户不应具有
DROP TABLE或更改 schema 的权限。 - 使用 ORM: 对象关系映射器(Object-Relational Mappers,例如 Laravel 中的 Eloquent、Python 中的 SQLAlchemy、Node.js 中的 TypeORM)通常默认使用预处理语句,增加了一层抽象和安全性。
- 验证和清理输入: 虽然预处理语句是主要防御手段,但您仍应在应用程序级别验证用户输入,以确保其符合预期格式(例如,电子邮件地址看起来像电子邮件)。
- 永不信任用户输入: 将所有来自客户端的数据(表单、URL 参数、cookie、API 请求)视为潜在恶意,直到另有证明为止。