SQL 注入与 NoSQL 注入防护

深入解析 SQL 注入攻击原理( Union、盲注、时间注入)、ORM 安全使用、NoSQL 注入(MongoDB)向量,以及 WAF 规则与数据库安全编码规范。

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 字符串。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「安全」更多文章

  1. Kubernetes安全体系:RBAC、PodSecurity与NetworkPolicy实战
  2. 安全合规与数据保护
  3. 渗透测试与红蓝对抗