SQL 注入是 Web 应用中最古老的漏洞之一,至今仍在 OWASP Top 10 中名列前茅。从经典 SQL 到现代 NoSQL 数据库,注入攻击始终威胁着数据安全。本文深入解析注入原理与防御体系。
1. SQL 注入基础
1.1 经典注入示例
// ❌ 字符串拼接(极度危险)
public User login(String username, String password) {
String sql = "SELECT * FROM users WHERE username = '" + username +
"' AND password = '" + password + "'";
// username = "admin' -- "
// 结果:SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'xxx'
// -- 注释掉后面,无需密码即可登录
}
// ✅ 参数化查询(PreparedStatement)
public User login(String username, String password) {
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, username); // 库自动转义
ps.setString(2, hashPassword(password));
return executeQuery(ps);
}
1.2 注入类型攻击
Union 注入(读取其他表数据)
-- 原始查询
SELECT title, content FROM posts WHERE id = 1
-- 注入后
SELECT title, content FROM posts WHERE id = 1
UNION SELECT username, password FROM users --
-- 结果:返回 posts 内容 + users 表的敏感字段
盲注(Boolean-based)
-- 通过页面差异判断条件真假
SELECT * FROM products WHERE id = 1 AND SUBSTRING(
(SELECT password FROM users WHERE username='admin'), 1, 1
) = 'a'
-- 页面正常 → 第一个字符是 'a'
-- 页面异常 → 不是 'a',继续试 'b'... 逐位爆破
时间盲注(Time-based)
-- 通过响应时间判断
SELECT * FROM products WHERE id = 1
AND IF(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a',
SLEEP(5), 0)
-- 如果响应 5 秒 → 字符猜对
-- 立即响应 → 字符错误
堆叠查询(Stacked Query)
-- 执行多条 SQL(部分数据库支持)
'; DROP TABLE users; --
'; INSERT INTO users (username, password) VALUES ('hacker','pw'); --
2. ORM 安全使用
2.1 JPA/Hibernate
// ✅ 参数化查询(JPQL)
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
// ❌ 拼接 JPQL(虽然 JPQL 不支持 UNION,但仍可绕过逻辑)
@Query(value = "SELECT * FROM users WHERE username = '" + username + "'", nativeQuery = true)
// ❌ Criteria API 的误用
Root<User> root = cq.from(User.class);
cq.where(cb.equal(root.get("status"), request.getParameter("status"))); ← 不可信!
// ✅ 使用位置参数
Root<User> root = cq.from(User.class);
cq.where(cb.equal(root.get("status"), Status.valueOf(sanitizedStatus)));
2.2 MyBatis
<!-- ✅ #{} 预编译 -->
<select id="findUser" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
<!-- ❌ ${} 字符串替换(危险!) -->
<select id="findUserUnsafe" resultType="User">
SELECT * FROM users WHERE id = ${id}
</select>
<!-- ❌ ORDER BY / 表名/列名不能用 #{} -->
<!-- 必须时,使用白名单校验 -->
<select id="sortUsers" resultType="User">
SELECT * FROM users ORDER BY
<choose>
<when test="sortField == 'name'">name</when>
<when test="sortField == 'createdAt'">created_at</when>
<otherwise>id</otherwise>
</choose>
</select>
2.3 ORM 并非万能
// ORM 仍然可能注入:
// ❌ EntityManager.createQuery 拼接
String ql = "SELECT u FROM User u WHERE u.name = '" + name + "'";
em.createQuery(ql);
// ❌ Native Query 拼接
String sql = "SELECT * FROM users WHERE id IN (" + String.join(",", ids) + ")";
em.createNativeQuery(sql);
// ✅ 正确做法:使用参数绑定
String sql = "SELECT * FROM users WHERE id IN (:ids)";
em.createNativeQuery(sql).setParameter("ids", idList);
3. NoSQL 注入
3.1 MongoDB 注入
// ❌ 字符串拼接查询(Node.js)
const user = db.users.findOne({
username: req.body.username,
password: req.body.password
});
// 攻击:req.body = { "username": "admin", "password": {"$ne": null} }
// MongoDB 查询变为:
// db.users.findOne({ username: "admin", password: { $ne: null } })
// 返回 password 不为 null 的第一个用户(绕过认证!)
// ✅ Mongoose Schema 校验 + 类型转换
const userSchema = new Schema({
username: { type: String, required: true },
password: { type: String, required: true }
});
// 或使用 $eq 防止运算符注入
const user = await User.findOne({
username: { $eq: req.body.username },
password: { $eq: hashPassword(req.body.password) }
});
3.2 MongoDB 注入防御
| 防御措施 | 说明 |
|---|---|
| Schema 校验 | Mongoose/ODM 类型严格校验 |
| $eq 查询 | 将输入视为值而非运算符 |
| 输入过滤 | 拒绝包含 $ 或 . 的键名 |
| 驱动配置 | serializeFunctions: false |
4. WAF(Web 应用防火墙)
4.1 WAF 规则示例
# ModSecurity CRS 规则示例
SecRule REQUEST_COOKIES|REQUEST_COOKIES_NAMES|REQUEST_FILENAME|ARGS_NAMES|ARGS|XML:/* \
"@rx (?i:(?:select\s*\*\s*from|(?:delete|drop|truncate)\s+table|union\s+select|insert\s+into|load_file|into\s*outfile))" \
"id:942100,phase:2,block,log,msg:'SQL Injection Attack Detected'"
# 检测常见 SQL 关键词出现在参数中
4.2 WAF 绕过技术
攻击者使用编码和变形绕过 WAF:
-- 大小写混合
UnIoN SeLeCt
-- URL 编码
%55%6E%69%6F%6E %53%65%6C%65%63%74
-- Unicode 绕过多字节编码
%u0055%u006E%u0069%u006F%u006E
-- 注释干扰
UN/**/ION SELECT
-- 空字节截断(旧 PHP)
id=1%00' UNION SELECT
结论:WAF 是辅助防线,不能替代参数化查询。
5. 安全编码规范
5.1 数据库连接安全
# 最小权限原则
# 应用账号只给必要权限
GRANT SELECT, INSERT, UPDATE ON myapp.* TO 'app_user'@'%';
REVOKE DELETE, DROP, ALTER ON myapp.* FROM 'app_user'@'%';
# 不同操作使用不同账号
# app_read: SELECT
# app_write: SELECT, INSERT, UPDATE
# app_admin: 全部(仅管理后台使用)
5.2 应用层防御 checklist
✅ 所有数据库查询使用参数化绑定(PreparedStatement)
✅ 禁止使用字符串拼接 SQL
✅ ORM 中禁用原生查询拼接
✅ 输入长度限制(防止超长注入)
✅ white list 校验(如 ORDER BY 列名)
✅ 错误信息不泄露数据库结构
✅ 数据库账号最小权限
✅ 开启数据库查询日志审计
✅ 部署 WAF 作为辅助防线
✅ 定期扫描依赖漏洞
6. 总结
SQL/NoSQL 注入防御的核心原则:
根本原因:将用户输入与 SQL 命令混淆
正确做法:
用户输入 ──► 参数化查询 ──► 数据库将其视为"值"而非"命令"
错误做法:
用户输入 ──► 字符串拼接 ──► 数据库将其解析为 SQL 语句的一部分
纵深防御:
1. 输入验证(白名单)
2. 参数化查询(核心防线)
3. ORM 安全使用
4. 最小权限数据库账号
5. WAF 检测
6. 日志审计与告警
注入漏洞看似古老,但每年仍有大量新漏洞被报出。根本原因是开发框架使用不当和动态查询需求。牢记:永远不要信任用户输入,永远不要拼接 SQL 字符串。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。