在 Web 安全领域,SQL 注入长期位居 OWASP Top 10 之列。随着 ORM(对象关系映射)框架的普及,很多开发者形成了一个根深蒂固的印象:用了 ORM,SQL 注入就与我无关了。事实真的如此吗?本文将从 ORM 的防护原理出发,分析它为什么不能完全杜绝 SQL 注入,并给出实际开发中应当遵循的安全实践。
ORM 为什么能防住大部分 SQL 注入
ORM 的核心防护机制是参数化查询。以常见的 ORM 为例,当你写下 User.objects.filter(name=username) 时,ORM 并不会把 username 的值直接拼接到 SQL 字符串里,而是生成类似 SELECT * FROM user WHERE name = ? 的语句,再把用户输入作为参数单独传给数据库驱动。数据库会将参数视为纯数据,而非可执行的 SQL 代码,从而在根本上阻断了注入的可能。
此外,ORM 通常会对表名、字段名做白名单校验,对数据类型做强制转换,这些都在一定程度上提高了攻击门槛。可以说,只要开发者始终使用 ORM 提供的标准查询 API,绝大多数经典 SQL 注入场景确实会被挡住。
但“完全防止”是一个危险的幻觉
问题在于,ORM 并不能覆盖所有数据库操作场景。以下几条路径,恰恰是 ORM 防护的盲区。
1. 原生 SQL 拼接
几乎每个 ORM 都提供了执行原生 SQL 的入口,比如 Django 的 raw()、SQLAlchemy 的 text()、Hibernate 的 createNativeQuery()。一旦开发者在这里手动拼接字符串:
User.objects.raw(f"SELECT * FROM user WHERE name = '{name}'")
ORM 的参数化机制就被完全绕过了,注入风险与直接写 JDBC 代码没有区别。
2. 动态表名、字段名与排序方向
参数化查询只能保护值,不能保护标识符(表名、列名)。当业务需要动态排序时,很多代码会这样写:
order_by = request.GET.get('sort')
queryset.order_by(order_by)
如果 order_by 未经过白名单校验,攻击者可以构造恶意字段名,在某些 ORM 实现中触发注入。类似地,extra()、raw() 中的表名拼接也是高危区域。
3. LIKE 查询与通配符注入
即使使用了参数化,LIKE 查询中的 % 和 _ 如果来自用户输入且未转义,虽然不构成传统 SQL 注入,但可能导致数据越权读取(通配符注入)。这属于业务逻辑层面的安全问题,ORM 不会替你处理。
4. 二次注入
用户输入先被安全地存入数据库,之后在另一个业务流程中被取出并拼接进 SQL。此时 ORM 在第一环节的保护已经失效,第二环节如果使用原生拼接,注入依然会发生。这类漏洞隐蔽性强,常常逃过代码审计。
5. ORM 框架自身漏洞
ORM 框架本身也是软件,历史上多次出现过 CVE 级别的注入漏洞。例如某些版本在特定查询构造下未能正确转义参数。依赖 ORM 而完全不做输入校验,等于把安全责任全部外包给第三方库的更新速度。
实际开发中应该怎么做
理解了 ORM 的边界,防护思路就清晰了:
- 优先使用 ORM 标准 API,能用
filter()、exclude()就不写原生 SQL。 - 必须用原生 SQL 时,坚持参数化。以 SQLAlchemy 为例,使用
text("... WHERE name = :name")配合params={"name": name},而不是 f-string 拼接。 - 动态标识符走白名单。排序字段、表名等只允许从预定义集合中选择,拒绝任何自由输入。
- 输入校验与最小权限并重。应用连接数据库的账号只授予必要权限,禁止
DROP、FILE等高危操作,即便注入发生也能限制损害范围。 - 定期更新 ORM 与数据库驱动,关注安全公告。
- 引入 SAST/DAST 工具,在 CI 流程中扫描原生 SQL 拼接等危险模式。
相关阅读
如果你对 Web 安全的其他层面也感兴趣,推荐继续阅读本站以下文章:
结语
ORM 是一道非常有效的防线,但它是降低风险,而非消除风险。把 ORM 当作 SQL 注入的“银弹”,恰恰是许多真实漏洞的成因。安全的本质在于纵深防御:ORM 参数化、输入白名单校验、数据库最小权限、持续的安全测试,缺一不可。只有理解了工具的能力边界,才能真正用好它。
未经允许不得转载:任鹏个人博客 » ORM 框架能完全防止 SQL 注入吗


朋友圈点赞图在线生成源码