内容提要
ORM 不能杜绝 SQL 注入,只是将风险转移到参数化覆盖不到的地方。常见漏洞有四类:原始查询拼接用户输入、排序列等标识符无法参数化、二次注入利用已存库的恶意数据、ORM 调用中混入 literal() 原始 SQL。修复方法:绑定参数、对标识符使用白名单、统一参数化所有查询、改用 ORM 操作符 API。
延伸解读
ORM 安全边界:参数化覆盖不到的地方
ORM 自动参数化其构建的查询,但风险转移到了开发者手动拼接 SQL 的地方。文章指出四类常见漏洞:原始查询拼接、标识符无法参数化、二次注入、literal() 混入。这些地方往往因为项目其他部分看似安全而被忽视,攻击者却优先寻找。理解 ORM 的安全边界,有助于在代码审查中聚焦这些高风险区域。
动态排序与标识符注入的防御
排序字段、表名等标识符无法用参数绑定,必须作为 SQL 字符串的一部分。文章示例中,sortBy 直接拼入 ORDER BY 导致注入。防御方法是使用白名单,仅允许预定义的列名,而不是尝试过滤或转义。白名单比黑名单更可靠,因为标识符的合法字符集有限,容易枚举。
二次注入:已存数据并非绝对安全
即使写入时参数化,从数据库读出的数据若再次拼接进未参数化的查询,仍会触发注入。文章中的注册流程安全存储了恶意用户名,但管理搜索功能将其直接拼接,导致注入。这提醒开发者:数据来源不可信,无论它是否经过数据库。所有查询都应参数化,包括使用自身存储数据的查询。
literal() 的隐蔽风险与替代方案
sequelize.literal() 允许插入原始 SQL,常被误用在看似安全的 ORM 调用中。文章示例中,Product.findAll 的 where 条件使用 literal 拼接用户输入,导致注入。应改用 ORM 的操作符 API(如 Op.gt),并验证输入类型。同时,全局搜索 literal(、fn( 等原始 SQL 入口,避免遗漏。
Q&A
ORM 能完全防止 SQL 注入吗?
不能。ORM 只是将风险转移到参数化覆盖不到的地方,比如原始查询、动态标识符、二次注入和 literal() 调用。
使用 ORM 时,哪些地方容易发生 SQL 注入?
常见有四类:1) 原始查询拼接用户输入;2) 排序列等标识符无法参数化;3) 二次注入利用已存库的恶意数据;4) ORM 调用中混入 literal() 原始 SQL。
如何修复 ORM 中的原始查询注入漏洞?
使用参数绑定机制,如 Sequelize 的 replacements,而不是手动拼接字符串。例如:sequelize.query('SELECT * FROM sales WHERE region = :region', { replacements: { region } })。
动态排序功能为什么会导致 SQL 注入?如何安全实现?
因为标识符(如列名)无法参数化,必须直接拼入 SQL。安全做法是使用白名单验证,例如只允许 'created_at', 'name', 'email' 等列。
什么是二次 SQL 注入?如何防范?
二次注入指恶意数据先被参数化存入数据库,之后在另一个未参数化的查询中被取出使用。防范方法是所有查询都参数化,包括使用自己数据库中的数据。
ORM 调用中的 literal() 有什么风险?如何避免?
literal() 会原样插入 SQL,导致注入。应改用 ORM 的操作符 API,如 Op.gt、Op.between 等,并验证输入类型。