考察点: 预编译、参数绑定、ORM框架、输入校验
🎬 开场:一个价值千万的单引号
2008年,某电商网站被黑客攻击,50万用户信息泄露,损失上千万。
黑客用的武器是什么?不是病毒,不是木马,而是一个简单的单引号:' 😱
这就是臭名昭著的 SQL注入攻击(SQL Injection)!
今天,让我们化身"白帽子"黑客,学习SQL注入的原理,然后再变身"防护专家",学会如何防御!🛡️
第一部分:什么是SQL注入? 🕵️
1.1 基础概念
SQL注入 是指攻击者通过在输入数据中插入恶意的SQL代码,欺骗应用程序执行非预期的数据库操作。
1.2 一个简单的例子 🌰
场景:用户登录
正常代码:
// 获取用户输入
String username = request.getParameter("username"); // 输入:admin
String password = request.getParameter("password"); // 输入:123456
// 拼接SQL(危险!)
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
// 实际执行:SELECT * FROM users WHERE username = 'admin' AND password = '123456'
看起来没问题?让我们看看黑客怎么操作! 😈
黑客的操作:
用户名输入:admin' OR '1'='1
密码输入:随便什么
拼接后的SQL:
SELECT * FROM users
WHERE username = 'admin' OR '1'='1' AND password = '随便什么'
分析:
username = 'admin'→ 可能为假'1'='1'→ 永远为真!- 整个条件:
假 OR 真= 真 - 结果:不需要密码,直接登录成功! 🚨
1.3 生活比喻 🏦
想象你去银行取钱:
正常流程:
柜员:"请出示身份证和密码"
你:"这是我的身份证123456,密码是666666"
柜员:(验证通过)"请取款"
SQL注入攻击:
你:"我的身份证是123456' OR '1'='1"
柜员:(懵了)"咦,这个条件永远成立,那就给钱吧..."
你:(拿到别人的钱跑路)
第二部分:SQL注入的类型 🎭
2.1 Union注入(联合查询注入)
目标: 查询敏感数据
-- 正常查询
SELECT id, title, content FROM articles WHERE id = 1;
-- 注入:id参数输入 "1 UNION SELECT username, password, email FROM users--"
SELECT id, title, content FROM articles WHERE id = 1
UNION SELECT username, password, email FROM users--;
-- 结果:不仅返回文章,还返回了所有用户的账号密码!
解释:
UNION可以合并多个查询结果--是MySQL的注释符,注释掉后面的代码- 攻击者获取到了users表的数据
2.2 报错注入
目标: 通过错误信息获取数据库信息
-- 输入:1' AND extractvalue(1, concat(0x7e, (SELECT password FROM users LIMIT 1))) --
-- MySQL会报错,但错误信息中包含了密码!
ERROR 1105: XPATH syntax error: '~admin123456'
原理: 故意触发错误,让错误信息暴露敏感数据
2.3 布尔盲注
场景: 页面不显示数据,只显示成功/失败
-- 猜测数据库名的第一个字符
-- 输入:1' AND SUBSTRING(DATABASE(), 1, 1) = 'a' --
SELECT * FROM articles WHERE id = 1 AND SUBSTRING(DATABASE(), 1, 1) = 'a';
-- 如果页面正常显示,说明数据库名第一个字母是'a'
-- 如果页面异常,说明不是'a',继续猜'b', 'c'...
-- 通过大量尝试,逐个字符猜出数据库名、表名、字段名、数据
就像玩"猜数字"游戏! 🎮
2.4 时间盲注
场景: 页面什么都不显示
-- 输入:1' AND IF(SUBSTRING(DATABASE(),1,1)='a', SLEEP(5), 0) --
SELECT * FROM articles WHERE id = 1 AND IF(SUBSTRING(DATABASE(),1,1)='a', SLEEP(5), 0);
-- 如果第一个字母是'a',页面会延迟5秒响应
-- 如果不是'a',立即响应
通过响应时间判断! ⏱️
2.5 堆叠注入(最危险)
目标: 执行多条SQL语句
-- 输入:1'; DROP TABLE users; --
SELECT * FROM articles WHERE id = 1; DROP TABLE users; --;
-- 第一条:查询文章
-- 第二条:删除用户表!💀
这就是传说中的 "小Bobby Tables" 梗! 😂

妈妈:学校打电话说你们数据库出问题了
儿子:他们没给学生表做参数化查询吗?
2.6 二次注入(隐蔽)
攻击流程:
- 第一步: 注册用户名
admin'--(被安全存入数据库) - 第二步: 修改密码时,取出用户名拼接SQL
UPDATE users SET password = '新密码' WHERE username = 'admin'--' -- 实际上修改了admin的密码!
特点: 第一次输入是安全的,第二次使用时触发注入
第三部分:SQL注入的危害 💥
3.1 数据泄露 📤
-- 获取所有用户信息
' UNION SELECT username, password, phone, email FROM users --
-- 获取管理员账号
' UNION SELECT * FROM admin_users WHERE role = 'admin' --
后果: 用户隐私泄露、商业机密被窃、违反GDPR等法规
3.2 数据篡改 ✏️
-- 修改商品价格
'; UPDATE products SET price = 0.01 WHERE product_id = 999 --
-- 修改用户权限
'; UPDATE users SET role = 'admin' WHERE username = 'hacker' --
后果: 数据完整性被破坏、业务逻辑混乱
3.3 数据删除 🗑️
-- 删除所有订单
'; DELETE FROM orders --
-- 清空数据库
'; DROP DATABASE mydb --
后果: 业务停摆、数据无法恢复(如果没备份)
3.4 获取服务器权限 🔓
-- MySQL的file权限
' UNION SELECT LOAD_FILE('/etc/passwd') --
-- 写入WebShell
' INTO OUTFILE '/var/www/html/shell.php' --
后果: 控制整个服务器,后果不堪设想
3.5 真实案例 📰
| 时间 | 事件 | 影响 |
|---|---|---|
| 2011 | Sony PlayStation Network被SQL注入攻击 | 7700万用户信息泄露 |
| 2012 | LinkedIn密码泄露 | 650万密码被盗 |
| 2015 | TalkTalk被攻击 | 15.7万用户敏感信息泄露,罚款40万英镑 |
| 2019 | 某电商平台 | 数千万用户数据被拖库 |
第四部分:防御方法(重点!)🛡️
4.1 预编译语句(PreparedStatement)⭐⭐⭐⭐⭐
这是最有效的防御方法!
❌ 不安全的代码(字符串拼接):
String username = request.getParameter("username");
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
✅ 安全的代码(预编译):
String username = request.getParameter("username");
String sql = "SELECT * FROM users WHERE username = ?"; // 使用占位符
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username); // 参数绑定
ResultSet rs = pstmt.executeQuery();
原理:
第一步:编译SQL模板
SQL模板:SELECT * FROM users WHERE username = ?
编译器:我知道这个SQL的结构了,username是参数位置
第二步:绑定参数
输入:admin' OR '1'='1
处理:把整个字符串当作字符串值,而不是SQL代码
实际查询:SELECT * FROM users WHERE username = 'admin' OR '1'='1'
第三步:执行
查询:找username等于 "admin' OR '1'='1" 的用户
结果:找不到(因为没有这个奇怪名字的用户)
关键: 参数和SQL代码分离,参数永远不会被当作SQL代码执行!
各语言示例:
Java(JDBC):
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
Python(MySQLdb):
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s",
(username, password))
PHP(PDO):
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);
Node.js(mysql2):
connection.execute(
"SELECT * FROM users WHERE username = ? AND password = ?",
[username, password],
function(err, results) { }
);
4.2 ORM框架(推荐)⭐⭐⭐⭐⭐
ORM(Object-Relational Mapping) 框架会自动处理SQL注入防护。
MyBatis(Java):
<!-- ✅ 安全:使用#{} -->
<select id="getUser" parameterType="String" resultType="User">
SELECT * FROM users WHERE username = #{username}
</select>
<!-- ❌ 危险:使用${} -->
<select id="getUser" parameterType="String" resultType="User">
SELECT * FROM users WHERE username = '${username}'
</select>
区别:
#{}→ 预编译(安全)${}→ 字符串拼接(危险)
Hibernate(Java):
// ✅ 安全
String hql = "FROM User WHERE username = :username";
Query query = session.createQuery(hql);
query.setParameter("username", username);
List<User> users = query.list();
Spring Data JPA:
// ✅ 安全(自动参数化)
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
SQLAlchemy(Python):
# ✅ 安全
user = session.query(User).filter(User.username == username).first()
4.3 输入验证和过滤 ⭐⭐⭐
白名单验证(最安全):
// 验证用户ID(只允许数字)
if (!userId.matches("^\d+$")) {
throw new IllegalArgumentException("无效的用户ID");
}
// 验证排序字段(只允许特定值)
List<String> allowedFields = Arrays.asList("id", "name", "created_at");
if (!allowedFields.contains(sortField)) {
throw new IllegalArgumentException("无效的排序字段");
}
黑名单过滤(不推荐,容易被绕过):
// ❌ 不可靠的黑名单
String input = input.replaceAll("'", ""); // 可以用双引号绕过
String input = input.replaceAll("(?i)union", ""); // 可以用UNIunionON绕过
String input = input.replaceAll("(?i)select", ""); // 可以用SeLeCt绕过
为什么黑名单不可靠?
- SQL注入技巧太多,防不胜防
- 攻击者总能找到绕过方法
- 可能误伤正常输入
正确的输入验证:
public class InputValidator {
// 验证用户名(字母数字下划线,3-20位)
public static boolean validateUsername(String username) {
return username.matches("^[a-zA-Z0-9_]{3,20}$");
}
// 验证邮箱
public static boolean validateEmail(String email) {
String regex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
return email.matches(regex);
}
// 验证整数范围
public static boolean validateIntRange(String input, int min, int max) {
try {
int value = Integer.parseInt(input);
return value >= min && value <= max;
} catch (NumberFormatException e) {
return false;
}
}
}
4.4 最小权限原则 ⭐⭐⭐⭐
数据库用户权限设置:
-- ❌ 不要给应用这些权限
GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'%'; -- 超级权限!
-- ✅ 只给必要的权限
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';
-- 只给SELECT, INSERT, UPDATE, DELETE权限
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'localhost';
-- 禁止DROP, ALTER等危险操作
-- 禁止访问系统表
-- 禁止FILE权限(读写文件)
原则:
- 不同模块用不同的数据库账号
- 只读模块只给SELECT权限
- 禁止使用root账号
4.5 转义特殊字符 ⭐⭐
注意: 这是备选方案,不如预编译!
// MySQL转义
public static String escapeMySQL(String input) {
if (input == null) return null;
return input
.replace("\", "\\") // 反斜杠
.replace("'", "\'") // 单引号
.replace(""", "\"") // 双引号
.replace("\n", "\n") // 换行
.replace("\r", "\r") // 回车
.replace("\0", "\0"); // 空字符
}
// 使用JDBC的转义函数(不推荐,用PreparedStatement)
String safe = conn.nativeSQL(input);
限制:
- 容易遗漏某些特殊情况
- 不同数据库转义规则不同
- 不如预编译可靠
4.6 使用存储过程 ⭐⭐⭐
-- 创建存储过程
DELIMITER //
CREATE PROCEDURE getUserByUsername(IN p_username VARCHAR(50))
BEGIN
SELECT * FROM users WHERE username = p_username;
END //
DELIMITER ;
-- Java调用
CallableStatement cstmt = conn.prepareCall("{call getUserByUsername(?)}");
cstmt.setString(1, username);
ResultSet rs = cstmt.executeQuery();
优点:
- SQL逻辑在数据库端,不暴露给应用
- 参数自动处理,安全性好
缺点:
- 维护成本高
- 不够灵活
- 移植性差
4.7 WAF(Web应用防火墙)⭐⭐⭐⭐
硬件/软件防火墙,拦截恶意请求
开源WAF:
- ModSecurity(Apache/Nginx模块)
- NAXSI(Nginx Anti XSS & SQL Injection)
云WAF:
- 阿里云WAF
- 腾讯云WAF
- AWS WAF
- Cloudflare WAF
配置示例(ModSecurity):
# 检测SQL注入关键词
SecRule ARGS "@detectSQLi" \
"id:1001,phase:2,block,msg:'SQL Injection detected'"
# 检测union注入
SecRule ARGS "@rx (?i:union.*select)" \
"id:1002,phase:2,block,msg:'Union SQL Injection'"
4.8 安全编码规范 ⭐⭐⭐⭐⭐
✅ 安全代码检查清单:
☑ 所有SQL都使用预编译(PreparedStatement)
☑ 动态表名/字段名用白名单验证
☑ 数值类型参数先做类型转换
☑ 字符串参数做长度限制
☑ 使用ORM框架的安全API
☑ 不在错误信息中暴露SQL语句
☑ 关闭数据库详细错误输出
☑ 定期代码审查
☑ 使用静态代码分析工具
静态代码分析工具:
- FindBugs(Java)
- SonarQube(多语言)
- Checkmarx(商业)
- Fortify(商业)
第五部分:实战演练 💻
5.1 脆弱代码示例
@Controller
public class UserController {
@Autowired
private JdbcTemplate jdbcTemplate;
// ❌ 危险!容易被SQL注入
@GetMapping("/user/search")
public List<User> searchUser(@RequestParam String keyword) {
String sql = "SELECT * FROM users WHERE username LIKE '%" + keyword + "%'";
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
}
// ❌ 危险!动态排序字段
@GetMapping("/user/list")
public List<User> listUsers(@RequestParam String sortBy) {
String sql = "SELECT * FROM users ORDER BY " + sortBy;
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
}
}
攻击示例:
GET /user/search?keyword=%' UNION SELECT null, username, password FROM admin_users--%
GET /user/list?sortBy=id;DROP TABLE users;--
5.2 安全代码改造
@Controller
public class UserController {
@Autowired
private JdbcTemplate jdbcTemplate;
// ✅ 安全:使用预编译
@GetMapping("/user/search")
public List<User> searchUser(@RequestParam String keyword) {
// 1. 输入验证
if (keyword == null || keyword.length() > 50) {
throw new IllegalArgumentException("Invalid keyword");
}
// 2. 使用预编译
String sql = "SELECT * FROM users WHERE username LIKE ?";
return jdbcTemplate.query(sql,
new Object[]{"%" + keyword + "%"},
new BeanPropertyRowMapper<>(User.class));
}
// ✅ 安全:白名单验证
@GetMapping("/user/list")
public List<User> listUsers(@RequestParam String sortBy) {
// 白名单验证
List<String> allowedFields = Arrays.asList("id", "username", "created_at");
if (!allowedFields.contains(sortBy)) {
sortBy = "id"; // 默认值
}
String sql = "SELECT * FROM users ORDER BY " + sortBy;
return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
}
}
5.3 MyBatis安全示例
<!-- ✅ 安全的写法 -->
<select id="searchUsers" resultType="User">
SELECT * FROM users
WHERE username LIKE CONCAT('%', #{keyword}, '%')
ORDER BY ${sortField} <!-- 注意:sortField需要白名单验证 -->
</select>
public List<User> searchUsers(String keyword, String sortField) {
// 白名单验证sortField
if (!Arrays.asList("id", "username", "created_at").contains(sortField)) {
sortField = "id";
}
return userMapper.searchUsers(keyword, sortField);
}
第六部分:检测与测试 🔍
6.1 手工测试
常用测试Payload:
-- 1. 单引号测试
'
''
`
"
-- 2. 注释符测试
--
-- -
/*
#
-- 3. UNION注入测试
' UNION SELECT NULL--
' UNION SELECT NULL, NULL--
' UNION SELECT NULL, NULL, NULL--
-- 4. 布尔注入测试
' AND '1'='1
' AND '1'='2
-- 5. 时间注入测试
' AND SLEEP(5)--
' WAITFOR DELAY '00:00:05'--
-- 6. 报错注入测试
' AND extractvalue(1, concat(0x7e, database()))--
6.2 自动化扫描工具
1. SQLMap(最强大)
# 基础扫描
sqlmap -u "http://example.com/user?id=1"
# 指定参数
sqlmap -u "http://example.com/search" --data="keyword=test" -p keyword
# 获取数据库
sqlmap -u "http://example.com/user?id=1" --dbs
# 获取表
sqlmap -u "http://example.com/user?id=1" -D mydb --tables
# 获取数据
sqlmap -u "http://example.com/user?id=1" -D mydb -T users --dump
2. Burp Suite(专业)
- Intruder模块:自动化注入测试
- Scanner模块:漏洞扫描
3. OWASP ZAP(免费)
# 启动ZAP
zap.sh -daemon -port 8080
# 扫描URL
zap-cli quick-scan http://example.com
6.3 持续集成检测
# GitLab CI配置
security_scan:
stage: test
script:
- npm install -g sqlmap
- sqlmap -u "$TEST_URL" --batch --level=5
- if [ $? -ne 0 ]; then exit 1; fi
第七部分:应急响应 🚨
7.1 发现被攻击怎么办?
立即行动清单:
☑ 1. 断开受影响的服务器网络连接
☑ 2. 保留日志和现场
☑ 3. 修复漏洞代码
☑ 4. 修改所有数据库密码
☑ 5. 检查是否有后门
☑ 6. 恢复数据(如果有备份)
☑ 7. 通知受影响的用户
☑ 8. 向监管部门报告(如需要)
☑ 9. 加强监控
☑ 10. 总结和改进
7.2 日志分析
-- 查看慢查询日志
SHOW VARIABLES LIKE 'slow_query_log%';
-- 分析可疑SQL
SELECT * FROM mysql.slow_log
WHERE sql_text LIKE '%UNION%'
OR sql_text LIKE '%DROP%'
OR sql_text LIKE '%--';
🎓 总结:防御SQL注入的黄金法则
优先级排序:
P0(必须):
✅ 所有SQL使用预编译(PreparedStatement)
✅ 使用ORM框架的安全API
✅ 最小权限原则
P1(强烈推荐):
✅ 输入验证(白名单)
✅ 错误信息不暴露SQL
✅ 使用WAF
P2(建议):
✅ 代码审查
✅ 安全测试
✅ 监控告警
记忆口诀 🎵
SQL注入危害大,
数据泄露真可怕。
预编译是王道,
参数绑定不能少。
ORM框架用得好,
输入验证别忘掉。
权限控制要最小,
定期审计很重要!
📚 面试要点
- SQL注入原理:拼接SQL导致恶意代码执行
- 预编译防御:参数和代码分离,这是最有效的方法
- ORM框架:
#{}vs${},知道区别 - 输入验证:白名单优于黑名单
- 真实案例:能举出1-2个著名案例
- 实战经验:如何在项目中落地SQL注入防护
最后警告: ⚠️
SQL注入不是"别人的事",可能就发生在你的项目中!
- 每个开发者都应该重视
- 安全是"质量",不是"成本"
- 预防胜于补救
记住:永远不要相信用户输入! 🔐
加油,安全工程师!💪