Skip to content

SQLite - 注入

SQL 注入(SQL Injection)是一种严重的安全漏洞,当应用程序将不可信的用户输入直接整合到其 SQL 查询中时,就会发生这种漏洞。恶意用户可以提供特制输入,使其脱离数据上下文并修改 SQL 查询本身,可能导致数据窃取、未经授权的访问、数据损坏或整个系统被攻破。

本章将教你预防这种攻击的现代标准方法。基本原则是:永远不要相信用户输入,永远不要通过将字符串与用户提供的数据拼接来格式化查询。

让我们想象一个简单的 Python 脚本,它从数据库中查找用户。一种常见但危险且有缺陷的方法是使用 f-string 或字符串拼接来构建查询:

# 警告:此代码存在 SQL 注入漏洞。请勿使用。
import sqlite3
# 用户输入(例如,来自 Web 表单)
username_input = "' OR '1'='1' --"
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
# 漏洞在这里:将用户输入直接嵌入到查询字符串中。
query = f"SELECT * FROM users WHERE username = '{username_input}' AND active = 1;"
print(f"Executing: {query}")
# Executing: SELECT * FROM users WHERE username = '' OR '1'='1' --' AND active = 1;
cursor.execute(query)
results = cursor.fetchall()
print("Results:", results) # 这很可能会返回表中的所有用户!

在上面的示例中,攻击者提供了输入 ' OR '1'='1' --。当此输入插入到查询中时,WHERE 子句变为 WHERE username = '' OR '1'='1' -- ...。'1'='1' 条件始终为真,而 -- 开始一个注释,导致原始查询的其余部分被忽略。攻击者已成功绕过登录检查。

防止 SQL 注入的正确和行业标准方法是使用参数化查询(也称为预处理语句)。这项技术将 SQL 命令与用户提供的数据分离。

通过参数化,你可以编写带有数据占位符(SQLite 中为 ?)的 SQL 查询。然后,将实际用户数据作为单独的参数提供给执行函数。数据库驱动程序(database driver)确保数据被严格视为值,而不是可执行代码。

以下是使用 Python 的 sqlite3 库的先前脚本的安全、修正版本:

# 这是执行查询的正确且安全的方法。
import sqlite3
username_input = "David" # 正常输入
# username_input = "' OR '1'='1' --" # 恶意输入
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
# 1. SQL 查询使用变量占位符 (?)。
query = "SELECT * FROM users WHERE username = ? AND active = 1;"
# 2. 用户输入作为单独的元组传递给 execute 方法。
# 库处理安全替换。
cursor.execute(query, (username_input,))
results = cursor.fetchall()
print("Results:", results) # 即使是恶意输入,这也会安全地查找不到用户并返回 []。

当恶意输入提供给参数化查询时,数据库会查找名称为字面字符串 ' OR '1'='1' -- 的用户。由于没有用户具有此名称,查询会正确返回空结果,攻击因此被阻止。

同样的原则也适用于其他语言。以下是使用 PDO (PHP Data Objects) 在现代 PHP 中实现的方法:

<?php
// 这是现代 PHP 中正确且安全的方法。
$userInput = "' OR '1'='1' --"; // 恶意输入
try {
$pdo = new PDO('sqlite:example.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 1. 使用占位符准备语句。
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username AND active = 1;');
// 2. 将用户输入绑定到占位符并执行。
$stmt->execute([':username' => $userInput]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($results); // 将正确返回一个空数组。
} catch (PDOException $e) {
echo 'Connection failed: ' . $e->getMessage();
}
?>

较旧的教程可能会提到诸如 sqlite_escape_string() 函数或 PHP 中的 magic_quotes_gpc 等概念。这些都已弃用,不应使用。

手动转义输入容易出错且通常不完整,会留下安全漏洞。黑名单(Blocklisting)或白名单(allowlisting)字符也是一种有缺陷的策略,可能被聪明的攻击者绕过。**始终使用数据库驱动程序提供的参数化查询。**这是对抗 SQL 注入最简单、最稳健的防御措施。