📌 今日关键词:SQL 注入、参数化查询、预编译、WAF
大家好,我是 数据库小学妹 👋
最近我遇到一个很多刚入行的新手DBA都会遇到的问题,学会了写SQL,但不知道SQL还能被“攻击”!今天整理下我的血泪教训 + 实战经验,让你彻底搞懂这个让无数开发者和 DBA 夜不能寐的"数据库隐藏地雷"——SQL注入!
一、SQL注入是什么?
先抛开那些晦涩的技术定义。
想象你是餐厅服务员,顾客在点餐本写:"我要一份红烧肉。"你把单子交给厨房做菜——正常逻辑。
如果有人偷偷加了一句:"顺便后厨的账本也给我算一下。"你没看清直接交给厨房——敏感信息就泄露了。
SQL 注入的本质就是:用户在输入的地方塞入额外指令,程序把这些指令当正常 SQL 执行,导致数据被窃取或篡改。
危害等级
| 危害类型 | 影响 |
|---|---|
| 数据泄露 | 用户密码、身份证号全被拖库 |
| 数据篡改 | 修改订单金额、余额清零 |
| 删除数据 | DROP TABLE 清空表 |
| 远程命令执行 | 通过存储过程执行系统命令 |
2017 年 Equifax 事件,1.47 亿用户个人信息泄露,很多就是通过 SQL 注入获取的。
二、SQL 注入是怎么发生的?
看一个简单的登录表单:
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysql_query($sql);
正常输入 zhangsan / 123456,生成的 SQL:
SELECT * FROM users WHERE username = 'zhangsan' AND password = '123456'
但如果黑客输入:
- username:
admin' OR '1'='1
SQL 变成:
SELECT * FROM users WHERE username = 'admin' OR '1'='1'
OR '1'='1' 永远是真,这条 SQL 会返回 users表所有用户数据!黑客直接用第一个账号登录了。
三种典型注入类型
1. 布尔盲注
页面报错不明显,但会根据条件返回不同内容。
?id=1 AND SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a'
页面内容相同→第 1 位不是'a'
页面内容变化→第 1 位是'a'
黑客逐字破解出所有内容。
2. 时间延迟注入
无法通过页面差异判断,但可通过响应时间判断。
?id=1 AND IF(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a',SLEEP(5),0)
响应时间长 5 秒→第 1 位是'a'
非常隐蔽的注入方式,常用于测试 WAF。
3. 联合查询注入
最直观易懂:
?id=1 UNION SELECT username,password,NULL FROM users--
直接把所有用户的用户名和密码合并到查询结果里显示!
三、为什么要用参数化查询?
错误的写法(危险)
username = input("请输入用户名:")
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
如果用户输入 admin' OR '1'='1,SQL 会变成:
SELECT * FROM users WHERE username = 'admin' OR '1'='1'
问题根源:程序把用户输入的整个字符串都当成了 SQL 语法的一部分。
正确的写法(安全)
username = input("请输入用户名:")
query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (username,))
即使用户输入 admin' OR '1'='1,生成的 SQL 实际上是:
SELECT * FROM users WHERE username = 'admin'' OR ''1''=''1'
关键区别:参数化查询将用户输入的内容当作纯字符串值,而不是 SQL 代码。即使包含特殊字符,也被当作普通字符处理。
对比下来:
| 特性 | 字符串拼接 | 参数化查询 |
|---|---|---|
| SQL 注入防护 | ❌ 无 | ✅ 100% |
| 性能 | ⚠️ 每次重新解析 | ✅ 可复用 |
只要用了参数化查询,SQL 注入就理论上不存在。没有例外!
四、各语言参数化查询正确写法
Python (Django/Flask)
# Django ORM 自动使用参数化
user = User.objects.filter(username=username, password=password).first()
# SQLAlchemy
user = User.query.filter_by(username=username).first()
Java
// JDBC
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
// Spring JPA
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
PHP
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
C# (.NET)
using (SqlCommand cmd = new SqlCommand("SELECT * FROM users WHERE username = @username", conn))
{
cmd.Parameters.AddWithValue("@username", username);
SqlDataReader reader = cmd.ExecuteReader();
}
五、三层防御体系
第一层:参数化查询(必须项)
上面已经讲了很多,这里不再赘述。记住一句话:所有数据库查询都用参数化。
第二层:输入验证(辅助项)
虽然参数化已足够安全,额外加一层校验也能提升体验。
import re
def validate_username(username):
"""只允许字母数字下划线,长度 3-20"""
if not re.match(r'^[a-zA-Z0-9_]{3,20}$', username):
return False
return True
常见字段校验规则:
| 字段 | 规则 | 示例 |
|---|---|---|
| 用户名 | 字母数字下划线 3-50 字符 | ^[a-zA-Z0-9_]{3,50}$ |
| 邮箱 | 符合 RFC 格式 | ^[^\s@]+@[^\s@]+\.[^\s@]+$ |
| 手机号 | 11 位数字 | ^1[3-9]\d{9}$ |
| ID 参数 | 正整数 | ^\d+$ |
注意:输入校验不能替代参数化查询!只能作为辅助措施。
第三层:最小权限原则(兜底项)
即使前两层被突破,限制权限也能减少损失。
-- ❌ 错误:使用 root 连接
GRANT ALL PRIVILEGES ON app_database.* TO 'app_user'@'%';
-- ✅ 正确:仅授予必要权限
GRANT SELECT, INSERT, UPDATE, DELETE ON app_database.users TO 'app_user'@'%';
REVOKE DROP, ALTER, CREATE ON app_database.* FROM 'app_user'@'%';
为不同的应用模块创建独立账号,做到风险隔离。
六、如何检测漏洞?
静态代码扫描
用 IDE 插件或专业工具自动扫描。
推荐工具:
- SonarQube
- FindSecBugs (Java)
- Bandit (Python)
动态模糊测试
构造各种特殊输入观察系统反应。
常用探测 Payload:
'
"
' OR '1'='1
" OR "1"="1
' --
' /*
' AND SLEEP(5) --
?id=1; SELECT SLEEP(10)
自动化测试工具:
- SQLMap(仅限授权测试!)
- Burp Suite
- OWASP ZAP
日志监控
生产环境部署日志监控,及时发现异常。
可疑日志特征:
SELECT * FROM users WHERE username = 'admin' OR '1'='1'
GET /login?user=admin' HTTP/1.1 500 Internal Server Error
GET /article?id=1; SELECT SLEEP(10) HTTP/1.1 200 OK (took 10s)
七、综合防护方案
不想手动实现每一层?这些现成方案可以直接用:
Web 应用防火墙(WAF)
阿里云 WAF、腾讯云 WAF、Cloudflare、ModSecurity(开源)。
Nginx + ModSecurity 配置示例:
Include crs/crs-setup.conf.d/900-rule-start.conf
SecRule REQUEST_BODY "@txDetectSqlInjection" \
"id:942100, phase:2, deny, msg:'SQL Injection Attack Detected'"
ORM 框架
使用 ORM 能让数据库操作更安全且易维护。
主流框架:Django ORM、Hibernate、Eloquent、Entity Framework
优点:自动生成参数化查询,防止大部分注入漏洞。
注意:即使是 ORM 也要避免直接使用原生 SQL 拼接!
代码审查清单
- [ ] 所有数据库查询都使用参数化查询?
- [ ] 是否存在字符串拼接 SQL?
- [ ] 用户输入是否有适当验证?
- [ ] 数据库账号权限是否最小化?
- [ ] 敏感数据是否加密存储?
- [ ] 是否有日志记录和监控?
八、常见误区
误区 1:"我只做内部系统,不会有攻击者"
内网系统也存在敏感数据,而且往往缺少安全防护,一旦被攻破后果同样严重。
误区 2:"我用了转义函数,应该没问题了吧?"
// ❌ 错误
$username = addslashes($_POST['username']);
$query = "SELECT * FROM users WHERE name = '$username'";
不同数据库的转义规则不同,编码问题可能导致失效,不如参数化彻底。
误区 3:"我的数据库没有公开 IP,很安全"
内网渗透是常见攻击路径,横向移动经常发生在企业内部网络中。
误区 4:"SQL 注入太老了,没人会用"
根据 OWASP Top 10,注入漏洞仍然是最常见的安全风险之一。
九、今日学习心得
- SQL 注入的本质是用户输入被当作 SQL 代码执行
- 参数化查询是防御 SQL 注入的唯一可靠方法
- 三层防御体系(参数化 + 验证 + 最小权限)效果最佳
- 定期进行安全测试和代码审查
- WAF、ORM 框架和安全工具可作为辅助手段
👋 我是 数据库小学妹 一个用设计师思维学数据库的转行人。我们一起,把复杂的安全知识变得简单易懂!💕
本文仅供学习和防御目的,请勿用于非法入侵或其他违法活动。网络安全请遵守相关法律法规!