SQL注入与安全
SQL注入与安全
🎯 生活类比
想象一个快递代收点:
- 正常流程:你说"帮我取张三的包裹",工作人员按名字查找 → 正常查询
- 注入攻击:你说"帮我取张三的包裹,顺便把李四的也给我",工作人员没仔细分辨,把两个人的包裹都给了你 → SQL 注入
- 防御措施:工作人员要求你填写完整的取件单,每个字段独立填写,不接受口头拼凑的话 → 参数化查询
📚 核心概念
1. SQL 注入原理
SQL 注入的本质是:用户输入被当作 SQL 代码执行。
当程序用字符串拼接构造 SQL 时,攻击者可以插入恶意 SQL 片段。
SQL
-- 后端代码(伪代码)
sql = "SELECT * FROM users WHERE username = '" + input_user + "' AND password = '" + input_pass + "'"
-- 正常输入
-- input_user = "admin"
-- input_pass = "123456"
-- 生成的 SQL:
SELECT * FROM users WHERE username = 'admin' AND password = '123456'
-- 恶意输入
-- input_user = "admin' --"
-- input_pass = "随便填"
-- 生成的 SQL:
SELECT * FROM users WHERE username = 'admin' --' AND password = '随便填'
-- '--' 是 SQL 注释,后面的条件被注释掉,绕过了密码验证!
2. 注入攻击常见手法
SQL
-- 1. 绕过登录验证
-- 输入: admin' OR '1'='1' --
SELECT * FROM users WHERE username = 'admin' OR '1'='1' --' AND password = ''
-- 2. 联合查询窃取数据
-- 输入: ' UNION SELECT username, password FROM users --
SELECT * FROM products WHERE name = '' UNION SELECT username, password FROM users --'
-- 3. 堆叠注入执行多条语句
-- 输入: '; DROP TABLE users; --
SELECT * FROM products WHERE name = ''; DROP TABLE users; --'
-- 4. 布尔盲注(通过返回差异推断数据)
-- 输入: ' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a' --
SELECT * FROM products WHERE name = '' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a' --'
3. 参数化查询(Prepared Statement)
核心思想:SQL 结构和数据分离,数据库引擎不会把参数值当作 SQL 代码执行。
SQL
-- ❌ 字符串拼接(危险)
SET @username = "admin' --";
SET @sql = CONCAT('SELECT * FROM users WHERE username = ''', @username, '''');
PREPARE stmt FROM @sql;
EXECUTE stmt;
-- ✅ 参数化查询(安全)
SET @username = "admin' --";
PREPARE stmt FROM 'SELECT * FROM users WHERE username = ?';
EXECUTE stmt USING @username;
-- 数据库会把整个 @username 当作一个字符串值,不会执行其中的 SQL 片段
各语言的参数化查询:
PYTHON
# Python + MySQL Connector
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))
# Python + SQLAlchemy ORM
user = session.query(User).filter(User.username == username, User.password == password).first()
JAVASCRIPT
// Node.js + mysql2
const [rows] = await pool.execute('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]);
// Node.js + Prisma ORM
const user = await prisma.user.findFirst({ where: { username, password } });
JAVA
// Java JDBC
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
PHP
// PHP PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute(['username' => $username, 'password' => $password]);
4. ORM 防注入
ORM(对象关系映射)框架通常自动使用参数化查询,但拼接原生 SQL 时仍需小心:
PYTHON
# ✅ ORM 自动参数化(安全)
User.objects.filter(username=username, password=password)
# ❌ ORM 中拼接原生 SQL(危险)
User.objects.raw(f"SELECT * FROM users WHERE username = '{username}'")
# ✅ ORM 中使用原生 SQL 的安全写法
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])
JAVASCRIPT
// ✅ Sequelize ORM 自动参数化
await User.findOne({ where: { username, password } });
// ❌ Sequelize 拼接查询(危险)
await sequelize.query(`SELECT * FROM users WHERE username = '${username}'`);
// ✅ Sequelize 安全写法
await sequelize.query("SELECT * FROM users WHERE username = ?", { replacements: [username] });
5. 最小权限原则
数据库用户只授予完成任务所需的最小权限:
SQL
-- 创建只读用户
CREATE USER 'app_readonly'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON mydb.* TO 'app_readonly'@'%';
-- 创建应用用户(只允许增删改查,不允许 DROP/ALTER)
CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'%';
-- 禁止 GRANT 权限传递
REVOKE GRANT OPTION ON mydb.* FROM 'app_user'@'%';
-- 管理员用户(仅限内部管理)
CREATE USER 'db_admin'@'localhost' IDENTIFIED BY 'very_strong_password';
GRANT ALL PRIVILEGES ON mydb.* TO 'db_admin'@'localhost';
6. 防御清单
| 防御措施 | 说明 |
|---|---|
| ✅ 参数化查询 | 所有 SQL 查询使用参数占位符 |
| ✅ ORM 框架 | 优先使用 ORM,减少手写 SQL |
| ✅ 输入验证 | 验证长度、类型、格式(白名单优于黑名单) |
| ✅ 最小权限 | 数据库用户只授予必要权限 |
| ✅ 错误信息 | 不向用户暴露数据库错误详情 |
| ✅ WAF 防护 | 部署 Web 应用防火墙 |
| ✅ 定期审计 | 检查代码中的 SQL 拼接 |
| ❌ 字符串转义 | 不够可靠,不作为唯一防线 |
💡 基本语法
SQL
-- MySQL Prepared Statement
PREPARE stmt FROM 'SELECT * FROM users WHERE id = ?';
SET @id = 1;
EXECUTE stmt USING @id;
DEALLOCATE PREPARE stmt;
-- MySQL LIKE 查询的安全写法
PREPARE stmt FROM 'SELECT * FROM users WHERE name LIKE ?';
SET @pattern = CONCAT('%', 'keyword', '%');
EXECUTE stmt USING @pattern;
-- IN 子句的安全写法(需要动态构造)
-- 方法:使用 FIND_IN_SET 或临时表
PREPARE stmt FROM 'SELECT * FROM users WHERE FIND_IN_SET(id, ?)';
SET @ids = '1,2,3';
EXECUTE stmt USING @ids;
💡 提示:永远不要信任用户输入。即使是下拉框、隐藏字段也可能被篡改,服务端必须对所有输入做校验和参数化处理。
示例:登录接口防注入改造(难度⭐)
改造前(危险):
PYTHON
# Python Flask - 危险写法
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
# ❌ 字符串拼接,存在 SQL 注入风险
sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
user = db.execute(sql).fetchone()
if user:
return "登录成功"
return "用户名或密码错误"
改造后(安全):
PYTHON
# Python Flask - 安全写法
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
# 输入验证
if not username or len(username) > 50:
return "用户名格式错误", 400
if not password or len(password) > 100:
return "密码格式错误", 400
# ✅ 参数化查询
user = db.execute(
"SELECT * FROM users WHERE username = ? AND password = ?",
(username, password)
).fetchone()
if user:
return "登录成功"
return "用户名或密码错误"
示例:搜索功能防注入与性能优化(难度⭐⭐)
危险写法:
JAVASCRIPT
// Node.js Express - 危险写法
app.get('/search', (req, res) => {
const keyword = req.query.q;
// ❌ 拼接 SQL,存在注入风险
const sql = `SELECT * FROM articles WHERE title LIKE '%${keyword}%' OR content LIKE '%${keyword}%'`;
db.query(sql, (err, results) => {
res.json(results);
});
});
安全 + 优化写法:
JAVASCRIPT
// Node.js Express - 安全 + 优化写法
app.get('/search', async (req, res) => {
try {
const keyword = req.query.q?.trim();
// 输入验证
if (!keyword || keyword.length > 100) {
return res.status(400).json({ error: '搜索关键词长度 1-100 个字符' });
}
// ✅ 参数化查询 + 全文索引优化
const sql = `
SELECT id, title, LEFT(content, 200) AS snippet, created_at
FROM articles
WHERE status = 1
AND MATCH(title, content) AGAINST(? IN BOOLEAN MODE)
ORDER BY MATCH(title, content) AGAINST(? IN BOOLEAN MODE) DESC
LIMIT 20
`;
const [rows] = await pool.execute(sql, [keyword, keyword]);
res.json(rows);
} catch (err) {
// 不暴露数据库错误详情
console.error('Search error:', err);
res.status(500).json({ error: '搜索服务暂时不可用' });
}
});
🏢 场景一:用户注册接口安全防护
PYTHON
# Python - 安全的用户注册
import re
import hashlib
@app.route('/register', methods=['POST'])
def register():
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
# 1. 输入验证(白名单)
if not re.match(r'^[a-zA-Z0-9_]{3,50}$', username):
return "用户名只能包含字母、数字和下划线,长度3-50", 400
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email) or len(email) > 100:
return "邮箱格式不正确", 400
if len(password) < 8 or len(password) > 100:
return "密码长度8-100个字符", 400
# 2. 参数化查询检查是否已存在
existing = db.execute(
"SELECT id FROM users WHERE username = ? OR email = ?",
(username, email)
).fetchone()
if existing:
return "用户名或邮箱已被注册", 409
# 3. 密码哈希存储
password_hash = hashlib.sha256(password.encode()).hexdigest()
# 4. 参数化插入
db.execute(
"INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)",
(username, email, password_hash)
)
db.commit()
return "注册成功", 201
🏢 场景二:动态筛选条件的安全构建
JAVASCRIPT
// Node.js - 安全构建动态查询
function buildProductQuery(filters) {
let sql = 'SELECT * FROM products WHERE 1=1';
const params = [];
if (filters.categoryId) {
sql += ' AND category_id = ?';
params.push(filters.categoryId);
}
if (filters.minPrice !== undefined) {
sql += ' AND price >= ?';
params.push(filters.minPrice);
}
if (filters.maxPrice !== undefined) {
sql += ' AND price <= ?';
params.push(filters.maxPrice);
}
if (filters.keyword) {
sql += ' AND name LIKE ?';
params.push(`%${filters.keyword}%`);
}
if (filters.status !== undefined) {
sql += ' AND status = ?';
params.push(filters.status);
}
// 排序字段白名单
const allowedSort = ['price', 'created_at', 'sales_count'];
const sortBy = allowedSort.includes(filters.sortBy) ? filters.sortBy : 'created_at';
const sortOrder = filters.sortOrder === 'ASC' ? 'ASC' : 'DESC';
sql += ` ORDER BY ${sortBy} ${sortOrder}`;
// 分页参数验证
const page = Math.max(1, parseInt(filters.page) || 1);
const pageSize = Math.min(100, Math.max(1, parseInt(filters.pageSize) || 20));
sql += ' LIMIT ? OFFSET ?';
params.push(pageSize, (page - 1) * pageSize);
return { sql, params };
}
// 使用
app.get('/products', async (req, res) => {
const { sql, params } = buildProductQuery(req.query);
const [rows] = await pool.execute(sql, params);
res.json(rows);
});
💡 关键:排序字段不能直接用参数化占位符(因为是列名不是值),必须用白名单校验。
❓ 常见问题
Q:参数化查询会影响性能吗? A: 不会。参数化查询还能利用数据库的预编译缓存,多次执行同一模板的 SQL 时反而更快。
Q:ORM 就一定安全吗? A: 不一定。ORM 的自动参数化是安全的,但如果使用
raw()或拼接字符串构造查询,仍然存在注入风险。框架只是工具,安全意识才是关键。
Q:存储过程能防注入吗? A: 存储过程本身不能防注入。如果存储过程内部使用了动态 SQL 拼接,同样存在注入风险。关键在于是否使用参数化。
Q:只用前端校验够吗? A: 绝对不够。前端校验可被绕过(直接调 API、修改请求)。所有安全校验必须在服务端执行。
📖 小节
本课深入讲解了 SQL 注入攻击与防御:
- 注入原理:用户输入被当作 SQL 代码执行,根源是字符串拼接
- 攻击手法:绕过登录、联合查询、堆叠注入、盲注等
- 核心防御:参数化查询(Prepared Statement),将 SQL 结构与数据分离
- ORM 框架:自动参数化,但拼接原生 SQL 时仍需警惕
- 最小权限:数据库用户只授予必要权限,禁止使用 root 连接应用
- 纵深防御:参数化 + 输入验证 + 权限控制 + 错误隐藏 + WAF
📝 作业
- 找出以下代码中的 SQL 注入风险并修复:SQL
query = "SELECT * FROM products WHERE category = '" + category + "' ORDER BY " + sortField - 为一个用户搜索功能编写安全的参数化查询代码(支持模糊搜索、分页、排序)。
- 为你的项目创建三个数据库用户:只读用户、应用用户、管理员用户,并分别授予合适的权限。



