sequelize.literal() 一用即破防,因其将字符串原样插入sql、不转义不绑定;仅限写死的无变量sql片段,如'now()';含用户输入(如req.query)即构成sql注入风险。

Sequelize.literal() 是 SQL 注入的快捷入口,只要用了它包用户输入,参数化就彻底失效。
为什么 literal() 一用就破防
literal() 的作用是把传入的字符串原样塞进生成的 SQL,不加引号、不转义、不绑定——它就是为“写死 SQL 片段”设计的。一旦你把 req.query.q、req.body.sortBy 这类不可信数据丢进去,等于亲手把攻击面焊死在查询语句里。
常见错误写法:
-
where: { [Op.or]: [fn('LOWER', literal(req.query.q))] }——literal()绕过所有参数化机制 -
attributes: [[literal(req.query.calc), 'computed']]—— 动态表达式直接执行,可能含子查询或 UNION -
order: [[literal(`${req.query.sortBy} ${req.query.order}`)]]—— 排序字段+方向全由用户控制,可注入id; DROP TABLE users--
literal() 的合法使用场景其实非常窄
它只适合写死的、无任何变量参与的 SQL 片段,比如固定函数调用、常量表达式或数据库特有语法(如 MySQL 的 JSON_EXTRACT 路径字面量)。
安全用法示例:
-
attributes: [[literal('NOW()'), 'created_at']]—— 字符串'NOW()'是确定的,无拼接 -
where: { status: { [Op.eq]: literal("'active'") } }—— 注意:连单引号都是写死的,不是插值 -
order: [[literal('updated_at DESC')]]—— 整个字符串静态,不含任何req或input
⚠️ 只要字符串里出现 ${...}、+ 拼接、或来自 req.* 的值,就立刻掉出安全区。
替代 literal() 的安全方案
绝大多数你以为“必须用 literal()”的地方,其实都有参数化替代路径:
- 需要动态列名?→ 白名单校验:
['id', 'name', 'email'].includes(req.query.sortBy),再用于order: [[req.query.sortBy, 'ASC']] - 需要函数包装字段?→ 用
fn()+ 参数化值:fn('LOWER', sequelize.col('username')),别把用户值塞进fn()第二参数 - 需要复杂计算字段?→ 改用
sequelize.cast()、sequelize.fn()组合已知字段,或拆到应用层计算 - 真要拼接 SQL 表达式?→ 改用
sequelize.query()+replacements,把变量全放在绑定层,SQL 字符串本身保持静态
最容易被忽略的危险组合点
开发者常以为 “用了 fn() 就算安全”,但实际风险藏在参数嵌套里:
-
fn('CONCAT', literal(req.query.prefix), sequelize.col('name'))—— 第一个参数是literal(),整条就破防 -
where: { [Op.and]: [literal(`age > ${req.query.minAge}`)] }—— 数字没引号?照样能注入注释或分号 - 在
include.on或having中用literal()包用户输入 —— 这些位置同样绕过参数化
真正该盯住的不是“有没有调用 literal()”,而是“这个字符串最终会不会变成 SQL 解释器执行的指令”。只要它带用户输入,就不是 literal,是 payload。











