跳到主要内容

SQL 注入防御

问题

什么是 SQL 注入?有哪些攻击方式?如何有效防御?

答案

一、SQL 注入原理

SQL 注入是通过在用户输入中嵌入恶意 SQL 代码,使应用执行非预期的 SQL 语句。

用户输入: ' OR 1=1 --
拼接后的 SQL: SELECT * FROM users WHERE name = '' OR 1=1 --' AND password = '...'
^^^^^^^^^^^^^^^^
永真条件,绕过认证

二、常见攻击类型

1. 经典注入(Union-Based)

-- 原始 SQL
SELECT name, email FROM users WHERE id = {user_input}

-- 攻击输入: 1 UNION SELECT username, password FROM admin_users --
-- 实际执行:
SELECT name, email FROM users WHERE id = 1
UNION SELECT username, password FROM admin_users --

-- 效果:查出 admin_users 表中的用户名和密码

2. 盲注(Blind Injection)

页面不直接返回 SQL 结果,但可以通过页面表现推断信息:

-- 布尔盲注:通过 true/false 逐个字符猜解
' AND SUBSTRING(database(), 1, 1) = 'a' --
' AND SUBSTRING(database(), 1, 1) = 'b' --

-- 时间盲注:通过响应时间判断
' AND IF(SUBSTRING(database(), 1, 1) = 'a', SLEEP(5), 0) --

3. 堆叠注入(Stacked Queries)

-- 攻击输入: 1; DROP TABLE users; --
-- 实际执行:
SELECT * FROM users WHERE id = 1; DROP TABLE users; --

4. 二次注入

数据第一次存储时被安全处理,但后续在其他 SQL 中被不安全地拼接使用:

Step 1: 注册用户名 admin'--
Step 2: 存储到数据库(正常存储,因为注册时用了参数化查询)
Step 3: 修改密码时:
UPDATE users SET password = 'new_pass' WHERE name = 'admin'--'
实际效果:修改了 admin 的密码

三、防御策略

核心防御:参数化查询(Prepared Statement)

所有 SQL 查询都必须使用参数化查询,这是最重要、最有效的防御手段。

// ❌ 字符串拼接(危险!)
const sql = `SELECT * FROM users WHERE name = '${username}'`;

// ✅ 参数化查询(安全)
// Node.js mysql2
const [rows] = await connection.execute(
'SELECT * FROM users WHERE name = ? AND email = ?',
[username, email]
);

// PostgreSQL pg
const result = await pool.query(
'SELECT * FROM users WHERE name = $1 AND email = $2',
[username, email]
);

为什么参数化查询安全?

  • SQL 语句和参数分开发送给数据库
  • 数据库先编译 SQL 语句(确定结构),再绑定参数
  • 参数永远是数据,不会被解释为 SQL 代码

ORM 防注入

// Prisma(天然安全,API 不支持 SQL 拼接)
const users = await prisma.user.findMany({
where: { name: username } // 自动参数化
});

// TypeORM(Query Builder 自动参数化)
const users = await userRepo
.createQueryBuilder('user')
.where('user.name = :name', { name: username }) // 参数化
.getMany();

// Prisma 原始查询也安全(tagged template 自动参数化)
const users = await prisma.$queryRaw`
SELECT * FROM users WHERE name = ${username}
`;
危险操作
// ❌ 即使使用 ORM,拼接原始 SQL 仍然危险
const sql = `SELECT * FROM users WHERE name = '${username}'`;
await prisma.$queryRawUnsafe(sql); // 不安全!

// ❌ 动态表名/列名不能用参数化
const table = req.query.table;
const sql = `SELECT * FROM ${table}`; // 不安全!
// ✅ 使用白名单校验
const allowedTables = ['users', 'posts', 'tags'];
if (!allowedTables.includes(table)) throw new Error('Invalid table');

输入验证(纵深防御)

// 类型验证
const id = parseInt(req.params.id, 10);
if (isNaN(id)) throw new BadRequestException('Invalid ID');

// 格式验证(使用 Zod)
import { z } from 'zod';

const schema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100).regex(/^[a-zA-Z0-9\u4e00-\u9fa5]+$/),
age: z.number().int().min(0).max(150),
});

const data = schema.parse(req.body); // 校验失败会抛 ZodError

最小权限

-- 应用使用的数据库账号只需要必要权限
CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'%';
-- 不授予 DROP, ALTER, CREATE, GRANT 等危险权限

-- 只读账号
CREATE USER 'readonly'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON mydb.* TO 'readonly'@'%';

WAF(Web 应用防火墙)

请求 → WAF(检测注入模式)→ 应用 → 数据库
↓ 拦截
恶意请求返回 403

WAF 是最外层的防护,但不能替代参数化查询(WAF 可被绕过)。

四、防御层次总结

层次措施有效性
必须参数化查询⭐⭐⭐⭐⭐
必须最小权限原则⭐⭐⭐⭐
推荐输入验证⭐⭐⭐
推荐ORM 框架⭐⭐⭐⭐
补充WAF⭐⭐⭐
补充错误信息隐藏⭐⭐

常见面试问题

Q1: 参数化查询为什么能防止 SQL 注入?

答案

参数化查询将 SQL 语句的结构数据分离:

  1. 数据库引擎先对 SQL 模板进行预编译,确定语法结构
  2. 然后将参数作为纯数据绑定到已编译的语句上
  3. 参数中的特殊字符('--;)被视为数据的一部分,不会改变 SQL 结构
模板编译: SELECT * FROM users WHERE name = ?
└── 此处只能是值
参数绑定: ? = "' OR 1=1 --"
实际效果: WHERE name = "' OR 1=1 --" ← 整个字符串作为值比较

Q2: 使用 ORM 就一定安全吗?

答案

不一定。ORM 的标准 API 是安全的,但以下场景仍可能注入:

  1. 拼接原始 SQL$queryRawUnsafe(userInput)
  2. 动态列名/表名orderBy(req.query.sortField)
  3. LIKE 通配符LIKE '%${input}%' 需要转义 %_
  4. ORM 的某些高级 API:如 TypeORM 的 where 接受字符串拼接

Q3: 如何检测项目中是否存在 SQL 注入风险?

答案

  1. 代码审查:搜索字符串拼接 SQL 的模式(+ "SELECT", 模板字符串包含 SQL)
  2. SAST 工具:SonarQube、Semgrep 等静态分析工具可检测注入风险
  3. 渗透测试:使用 sqlmap 等工具进行自动化测试
  4. ESLint 规则:自定义规则禁止危险的 SQL 拼接方式

相关链接