Skip to content

SQL Injection

SQL 注入是一种严重的 Web 安全漏洞,它允许攻击者干扰应用程序对数据库进行的查询。攻击者可能能够查看通常无法检索的数据、修改或删除数据,有时甚至获得数据库服务器的管理控制权。

对于任何与数据库打交道的开发人员来说,理解这一威胁至关重要。

SQL 注入如何在 Web 应用程序中发生

Section titled “SQL 注入如何在 Web 应用程序中发生”

Web 应用程序通常使用用户提供的输入动态地构建 SQL 查询。例如,当用户搜索产品或登录时,他们的输入可能会直接纳入 SQL 查询中。

考虑一个简化的服务器端脚本(伪代码)用于检索用户详细信息:

// Pseudo-code: Get user input from a web request
// 伪代码:从 Web 请求中获取用户输入
string userIdInput = getRequestString("UserId");
// Dynamically construct SQL query (VULNERABLE!)
// 动态构建 SQL 查询(存在漏洞!)
string sqlQuery = "SELECT * FROM Users WHERE UserId = " + userIdInput;

如果 userIdInput 例如是 123,则查询变为:SELECT * FROM Users WHERE UserId = 123;。这按预期工作。

然而,如果攻击者提供恶意输入,他们可以改变 SQL 查询的结构。

使用上面有漏洞的代码,如果攻击者输入 105 OR 1=1 作为 UserId,查询将变为:

SELECT * FROM Users WHERE UserId = 105 OR 1=1;

由于 1=1 始终为真,WHERE 子句对于所有行都有效。此查询将返回 Users 表中的所有记录,可能暴露所有用户名和密码等敏感数据。

2. 基于字符串输入和 ''='' (恒为真)的注入

Section titled “2. 基于字符串输入和 ''='' (恒为真)的注入”

考虑一个登录场景:

// Pseudo-code: Get login credentials
// 伪代码:获取登录凭据
string userNameInput = getRequestString("UserName");
string userPassInput = getRequestString("Password");
// VULNERABLE query construction
// 有漏洞的查询构建
string sqlQuery = "SELECT * FROM Users WHERE Name = '" + userNameInput + "' AND Pass = '" + userPassInput + "';";

如果攻击者在 UserName 中输入 admin' --(其中 -- 在 SQL 中表示注释开始),查询可能变为:

SELECT * FROM Users WHERE Name = 'admin' --' AND Pass = '...irrelevant...';

这可能使攻击者无需密码即可登录为 ‘admin’。另一个常见的输入是在用户名和密码字段都输入 ' OR '1'='1,可能绕过认证。

某些数据库允许使用分号分隔的多个 SQL 语句。如果攻击者可以注入分号,他们就可以附加恶意命令。

如果 userIdInput 是 105; DROP TABLE Products,我们第一个示例中的查询就变成了:

SELECT * FROM Users WHERE UserId = 105; DROP TABLE Products;

如果数据库用户拥有足够的权限,这可能会删除整个 Products 表。

主要防御方法:参数化查询(预处理语句)

Section titled “主要防御方法:参数化查询(预处理语句)”

防止 SQL 注入最有效的方法是始终使用参数化查询(也称为预处理语句 Prepared Statements)。使用参数化查询,您首先使用占位符定义 SQL 查询结构,然后将用户输入作为参数传递。

数据库驱动程序随后会确保输入被视为数据,而不是可执行的 SQL 代码。这意味着攻击者无法改变查询的意图。

使用伪代码进行数据库交互的示例:

string userIdInput = getRequestString("UserId");
// Define the SQL query with a placeholder (e.g., @userId, ?, :userId)
// 使用占位符定义 SQL 查询(例如,@userId, ?, :userId)
string sqlQuery = "SELECT * FROM Users WHERE UserId = @userId_param;";
// Create a database command object
// 创建数据库命令对象
var command = createDbCommand(sqlQuery);
// Add the user input as a parameter
// 将用户输入添加为参数
command.addParameter("@userId_param", userIdInput);
// Execute the command
// 执行命令
var results = command.executeQuery();

即使 userIdInput 是 105 OR 1=1,数据库也会将整个字符串视为要在 UserId 列中搜索的值,而不是 SQL 逻辑的一部分。它将搜索用户 ID 字面量为 ‘105 OR 1=1’ 的用户,这种情况不太可能存在,因此不会造成任何伤害。

其他防御层(重要但次要):

  • 使用 ORM(对象关系映射器): 现代 ORM,如 Entity Framework (C#)、Hibernate (Java)、SQLAlchemy (Python)、TypeORM (Node.js) 等,通常默认使用参数化查询,提供了一层很好的保护。
  • 输入验证: 根据预期的格式、类型和长度验证用户输入。例如,如果用户 ID 应该是一个数字,请确保它是一个数字。这是一个好习惯,但不能替代参数化查询,因为聪明的攻击者通常可以绕过简单的验证。
  • 最小权限原则: 确保您的应用程序使用的数据库用户账户只拥有最少的必要权限。例如,Web 应用程序可能不需要 DROP TABLE 的权限。
  • 尽可能避免动态查询构建: 如果查询结构是静态的,风险就会降低。
  • 保持软件更新: 定期更新您的数据库管理系统、Web 服务器和应用程序框架,以修补已知漏洞。

不该做的事情:依赖于黑名单过滤(过滤掉 SELECT、DROP 等词)或手动转义字符是容易出错且通常不足够的。攻击者善于找到绕过这类防御的方法。

以下示例展示了如何实现参数化查询。假设用户输入(txtUserID、txtNam、txtAdd、txtCit)是从 HTTP 请求中获取的。

C# 使用 ADO.NET(例如 ASP.NET):

string connectionString = "..."; // Your database connection string
// 您的数据库连接字符串
string userId = Request.QueryString["UserId"]; // Example: get from query string
// 示例:从查询字符串获取
using (SqlConnection connection = new SqlConnection(connectionString))
{
string sql = "SELECT * FROM Customers WHERE CustomerId = @CustomerIdParam;";
SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@CustomerIdParam", userId);
connection.Open();
SqlDataReader reader = command.ExecuteReader();
// Process reader
// 处理读取器
}

PHP 使用 PDO:

$customerName = $_POST['CustomerName']; // Example: get from POST data
// 示例:从 POST 数据获取
$address = $_POST['Address'];
$city = $_POST['City'];
$dsn = 'mysql:host=localhost;dbname=mydatabase';
$username = 'dbuser';
$password = 'dbpass';
try {
$pdo = new PDO($dsn, $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "INSERT INTO Customers (CustomerName, Address, City) VALUES (:name, :address, :city)";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':name', $customerName);
$stmt->bindParam(':address', $address);
$stmt->bindParam(':city', $city);
$stmt->execute();
echo "New record created successfully";
// echo "新记录创建成功";
} catch (PDOException $e) {
echo "Error: " . $e->getMessage();
// echo "错误:" . $e->getMessage();
}

Python 使用 sqlite3(对于其他数据库连接器如 PostgreSQL 的 psycopg2 或 MySQL 的 mysql.connector 原理类似):

import sqlite3
userId_input = "105" # Example input
# 示例输入
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
# Use '?' as placeholder
# 使用 '?' 作为占位符
sql_query = "SELECT * FROM Users WHERE UserId = ?"
cursor.execute(sql_query, (userId_input,))
results = cursor.fetchall()
for row in results:
print(row)
conn.close()

始终优先使用参数化查询/预处理语句。这是防止 SQL 注入最重要的一道防线。

延伸阅读:请查阅 OWASP (开放式 Web 应用安全项目) 网站,获取关于 SQL 注入和其他 Web 漏洞的全面信息:OWASP SQL Injection