原理

应用程序把用户输入拼接到 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 安全的知识整理。