原理
应用程序把用户输入拼接到 SQL 语句中,导致用户可以改变 SQL 的结构。
危险代码:
sql = "SELECT * FROM users WHERE name = '" + username + "'"正常输入:alice → SELECT * FROM users WHERE name = 'alice' ✅
恶意输入:' OR '1'='1 →
SELECT * FROM users WHERE name = '' OR '1'='1''1'='1' 恒为真 → 返回所有用户。
如果在登录场景,攻击者就绕过了认证。
危害
- 绕过认证(登录任意账号);
- 读取任意数据(用户表、支付信息);
- 修改/删除数据(
DROP TABLE、UPDATE); - 某些数据库配置下可以读写文件、执行系统命令;
- 拖库:一次性获取整个数据库。
分类
一、经典注入:结果直接返回到页面(容易利用)。
二、盲注(Blind SQLi):页面不返回数据,但可以通过布尔条件(页面是否有变化)或时间延迟(如 IF(condition, SLEEP(5), 0))逐位推断数据。
慢,但可行——自动化工具可以高效地完成。
三、带外注入(Out-of-band):通过让数据库发起 DNS 或 HTTP 请求,把数据带出来。适用于前两者都不可行的场景。
四、二阶注入:恶意数据先被存储到数据库(经过了转义,所以第一次插入是安全的),但后续查询中从数据库读出后再次拼接进 SQL——此时转义已经"失效"(因为数据被读出时是原始形式)。
这提醒我们:转义不是根本解决方案。
防御
一、参数化查询(预编译语句)——根本解决方案 ✅
原理:SQL 语句的结构先被送到数据库编译,用户输入作为参数单独传递。
参数永远不会被当作 SQL 代码解析。
# 正确
cursor.execute("SELECT * FROM users WHERE name = %s", (username,))// 正确
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, username);这是唯一可靠的根本解决方案。
即使用户输入 ' OR '1'='1,数据库也只会把它当作一个字符串值去匹配 name 字段——不会改变 SQL 的结构。
二、ORM 框架
大多数 ORM(如 SQLAlchemy、Hibernate、Django ORM)默认使用参数化查询——用它们就安全。
但要注意:ORM 通常提供"原生 SQL"的逃生口(如 Django 的 raw()、SQLAlchemy 的 text())。
在这些地方,必须自己使用参数化。
# Django:危险
User.objects.raw("SELECT * FROM users WHERE name = '%s'" % username)
# 安全
User.objects.raw("SELECT * FROM users WHERE name = %s", [username])三、存储过程(需谨慎)
存储过程本身不自动安全——如果在存储过程中拼接 SQL,同样有注入风险。
只有在存储过程内部使用参数化时,它才是安全的。
四、转义(不推荐作为主要手段)
对特殊字符转义(如把 ' 变成 \')可以防御部分注入,但:
- 依赖正确的字符集处理(历史上出现过因字符集问题绕过转义的案例);
- 不同的上下文需要不同的转义;
- 二阶注入可以绕过它;
- 容易遗漏。
结论:转义是最后的退路,不是首选方案。
五、最小权限(纵深防御)
数据库连接使用的账号应该:
- 只拥有必要的表的必要权限;
- 不用管理员/SA 账号连接应用;
- 禁止不必要的操作(如
DROP、FILE权限)。
这样即使发生注入,损害也受限。
六、输入验证(辅助)
对类型、长度、格式做校验(如 ID 必须是数字)。
注意:这只是辅助,不能替代参数化——因为很多场景下输入需要包含特殊字符(如文章内容本身)。
七、WAF(缓解)
能拦截常见的自动化注入扫描——但可被绕过(前文提到过 WAF 的局限)。
检测
手工:输入 ' 看是否报错;输入 ' OR '1'='1 看行为是否异常。
自动化:SQLMap 等工具。
代码层面:静态分析(SAST)工具可以识别字符串拼接构造 SQL 的模式。
一句话
唯一的根本解决方案是:永远使用参数化查询,绝不拼接 SQL。
这不是"最佳实践",而是必须遵守的底线。
说明:本文为 Web 安全的知识整理。