sequelize.query() 是唯一需手动防注入的地方,因其绕过orm参数化绑定;literal() 和 fn() 会原样输出用户输入,构成注入风险;sql结构部分(如列名、表名)无法参数化,须用白名单校验。

sequelize.query() 是唯一需要你手动防注入的地方
Sequelize 的 findAll、update、destroy 等模型方法默认走参数化绑定,天然防注入;唯独 sequelize.query() 绕过所有 ORM 层校验,直接执行 SQL 字符串——安全与否全由你传参方式决定。
常见错误是以为“加了 AS 或写了 WHERE 就安全”,其实只要字符串里出现 ${req.query.id} 或 + 拼接,就等于把攻击面裸露出去。
- ✅ 安全写法:用
replacements或bind参数传值,例如sequelize.query("SELECT * FROM users WHERE status = :status", { replacements: { status: req.body.status } }) - ✅ 位置占位符也行:
sequelize.query("SELECT * FROM users WHERE id = ?", { replacements: [req.params.id] }) - ❌ 危险写法:
sequelize.query(`SELECT * FROM users WHERE id = ${req.params.id}`)—— 模板字符串插值不经过任何转义 - ⚠️ 注意:
:status(命名绑定)和?(位置绑定)不能混用在同一查询中,否则会报错
literal() 和 fn() 不是“加个函数就安全”,而是注入快捷键
Sequelize.literal() 的设计目标就是“原样输出”,它不加引号、不转义、不绑定——等于主动关闭参数化。一旦把 req.query.sortBy 或 req.body.q 塞进去,就等同于把用户输入直接焊进 SQL。
典型破防场景:
- ❌
where: { [Op.or]: [fn('LOWER', literal(req.query.q))] }——literal()把用户输入直接当 SQL 片段执行 - ❌
order: [[literal(`${req.query.sortBy} ${req.query.order}`)]]—— 可注入id; DROP TABLE users-- - ✅ 安全用法仅限写死内容:
attributes: [[literal('NOW()'), 'created_at']]或order: [[literal('updated_at DESC')]]
只要字符串里含 ${}、+、或任何来自 req.* 的值,就立刻掉出安全区。
列名、表名、排序字段这些 SQL 结构位置无法参数化
SQL 的结构部分(如字段名、表名、ORDER BY 后的字段、GROUP BY 表达式)不能像值一样被参数化绑定。Sequelize 不会对它们做任何转义或校验,拼进去就直接执行。
比如这个写法看着像参数化,实则高危:attributes: [[req.query.sortBy, 'alias']] 或 where: { [req.query.field]: req.query.value }。
- 必须在 Controller 层做白名单校验,硬编码允许列表:
const allowedFields = ['id', 'name', 'email', 'created_at'] - 取值前严格比对:
const field = allowedFields.includes(req.query.field) ? req.query.field : 'id' - 禁止从数据库读白名单、禁止用正则“过滤单引号”——攻击者早就能绕过这类弱校验
Model 定义本身不防注入,但能帮你守住 where 边界
Model.init() 里写的 DataTypes.STRING 或 validate: { isEmail: true } 不参与 SQL 构建,也不替代参数化。它的作用是:让 Sequelize 在绑定参数时按指定类型处理(比如数字自动当 INT),并配合运行时校验提前拦截明显畸形输入(如 admin'--)。
真正起效的是你后续怎么用这些字段:
- ✅
where: { email: req.query.email }—— 值会被自动当作字符串参数绑定 - ❌
where: { [req.query.column]: req.query.value }—— 方括号是 JS 语法,但最终生成的是裸字段名,无任何防护 - ⚠️
attributes选项本身安全,但一旦塞入动态列名,就变成注入入口
复杂点在于:SQL 注入风险不在“有没有调用 validate”,而在于“这个变量是否出现在 SQL 结构位置”。白名单校验必须落在 Controller,不能交给 Model 或中间件兜底。











