
什么是SQL注入?
SQL注入是一种代码注入攻击,攻击者将SQL命令插入输入字段,然后由数据库执行。二十多年来,它一直是排名第一的Web应用程序漏洞,并且仍然频繁出现在安全事件报告中。
核心问题:通过拼接用户输入来构建SQL查询的应用程序,允许攻击者修改查询的逻辑。

经典示例
考虑一个登录表单,其后端查询如下:
// ❌ 存在漏洞——永远不要这样做
const query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
正常输入 username = "alice" 时,查询变为:
SELECT * FROM users WHERE username = 'alice' AND password = 'secret'
现在攻击者输入 username = "' OR '1'='1' --":
SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = '...'
-- 注释掉了查询的其余部分。'1'='1' 始终为真。攻击者无需知道任何密码即可登录为数据库中的第一个用户——通常是管理员。
更危险的注入模式
数据提取(UNION攻击)
' UNION SELECT username, password, null FROM users --
如果应用程序显示查询结果,攻击者可以从任何表中提取任何数据。
盲SQL注入
当应用程序不显示查询结果时,攻击者使用时间或布尔响应逐位提取数据:
' AND SLEEP(5) -- -- MySQL:如果存在漏洞则延迟5秒
' AND 1=(SELECT 1 FROM users WHERE username='admin' AND SUBSTRING(password,1,1)='a') --

数据库破坏
'; DROP TABLE users; --
'; DELETE FROM orders WHERE 1=1; --
防御:参数化查询(预编译语句)
根本修复:永远不要将用户输入拼接到SQL中。使用参数化查询,其中SQL结构固定,数据单独传递。
// ✅ Node.js with pg (PostgreSQL)
const { rows } = await pool.query(
'SELECT * FROM users WHERE username = $1 AND password_hash = $2',
[username, passwordHash]
);
// ✅ Node.js with mysql2
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = ? AND password_hash = ?',
[username, passwordHash]
);
// ✅ Node.js with better-sqlite3
const stmt = db.prepare('SELECT * FROM users WHERE username = ? AND password_hash = ?');
const user = stmt.get(username, passwordHash);
使用参数化查询时,数据库分别接收SQL结构和参数。即使参数包含SQL语法,它也被视为字面字符串——永远不会作为SQL命令执行。
# ✅ Python with psycopg2 (PostgreSQL)
cur.execute(
"SELECT * FROM users WHERE username = %s AND password_hash = %s",
(username, password_hash)
)
# ✅ Python with SQLite
cur.execute(
"SELECT * FROM users WHERE username = ? AND password_hash = ?",
(username, password_hash)
)
# ✅ Python with SQLAlchemy (ORM)
from sqlalchemy import text
result = db.execute(
text("SELECT * FROM users WHERE username = :username"),
{"username": username}
)
// ✅ Go with database/sql
row := db.QueryRow(
"SELECT id, username FROM users WHERE username = $1",
username,
)
ORM:默认参数化
对象关系映射器自动生成参数化查询,使安全路径成为简单路径:
// ✅ Prisma — 默认安全
const user = await prisma.user.findFirst({
where: {
username: username, // 自动参数化
passwordHash: passwordHash,
},
});
// ✅ Sequelize
const user = await User.findOne({
where: { username, passwordHash },
});
// ✅ TypeORM
const user = await userRepo.findOne({
where: { username, passwordHash },
});
警告: 如果使用带字符串插值的原始查询方法,ORM仍然可能存在漏洞:
// ❌ 仍然存在漏洞——在ORM中使用原始SQL
const user = await prisma.$queryRaw`SELECT * FROM users WHERE username = ${username}`;
// 等等——这实际上是安全的!Prisma使用带自动参数化的标签模板字面量
// ❌ 这确实存在漏洞:
const user = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE username = '${username}'` // 永远不要这样做
);
输入验证:纵深防御
参数化查询是主要防御手段。输入验证增加了第二层:
// 白名单验证——只接受预期字符
function validateUsername(username) {
if (!/^[a-zA-Z0-9_]{3,50}$/.test(username)) {
throw new Error('用户名格式无效');
}
return username;
}
// 类型检查
function validateUserId(id) {
const parsed = parseInt(id, 10);
if (isNaN(parsed) || parsed <= 0) {
throw new Error('用户ID无效');
}
return parsed;
}
验证减少了攻击面,但永远不能替代参数化查询。攻击者会找到绕过验证的创造性方法;参数化查询在结构上是安全的。

动态查询:处理列名和表名
参数适用于值,但不适用于列名、表名或SQL关键字。如果必须动态构建这些,请使用白名单:
// ✅ 安全:动态列名的白名单方法
const ALLOWED_SORT_COLUMNS = new Set(['name', 'email', 'created_at', 'updated_at']);
function buildSortQuery(column, direction) {
if (!ALLOWED_SORT_COLUMNS.has(column)) {
throw new Error(`无效的排序列:${column}`);
}
const dir = direction === 'DESC' ? 'DESC' : 'ASC'; // 强制有效值
return `ORDER BY ${column} ${dir}`; // 安全:两个值都来自白名单
}
const sql = `SELECT * FROM users ${buildSortQuery(req.query.sort, req.query.dir)}`;
永远不要使用黑名单(试图过滤掉坏字符)。攻击者知道数百种编码技术可以绕过它们。
最小权限:限制注入发生时的损害
即使使用参数化查询,纵深防御也很有价值:
-- 为应用程序创建一个受限数据库用户
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';
-- 只授予应用程序所需权限
GRANT SELECT, INSERT, UPDATE ON myapp.users TO 'app_user'@'localhost';
GRANT SELECT, INSERT ON myapp.orders TO 'app_user'@'localhost';
-- 不要授予DELETE、DROP或系统表访问权限
REVOKE ALL ON *.* FROM 'app_user'@'localhost';
如果注入攻击以某种方式成功,受限数据库用户会限制攻击者能做什么。
检测SQL注入漏洞
手动测试: 在输入字段中尝试以下内容:
'
''
'--
' OR '1'='1
' OR 1=1 --
如果应用程序抛出数据库错误或行为不同,则可能存在漏洞。
自动扫描: OWASP ZAP、sqlmap(仅用于授权测试)、Burp Suite Pro。
代码审查: 搜索带有SQL的字符串拼接:
grep -r "SELECT.*+.*req." src/
grep -r "WHERE.*`${" src/
总结
| 方法 | 有效性 | 使用时机 |
|---|---|---|
| 参数化查询 | ✅ 主要防御 | 始终——用于所有用户输入值 |
| ORM | ✅ 默认安全 | 用于标准CRUD操作 |
| 输入验证 | ✅ 纵深防御 | 在参数化之外额外使用 |
| 动态SQL白名单 | ✅ 用于标识符 | 列名、表名、ORDER BY |
| 转义/编码 | ⚠️ 仅作为最后手段 | 仅在无法参数化时 |
| 黑名单过滤 | ❌ 不足 | 永远不要单独依赖 |
→ 在开发过程中使用 SQL格式化工具 检查和格式化SQL查询。