replacements 选项传递的输入值没有被正确转义,在特定查询构造下可能导致 SQL 注入。Sequelize 是 Node.js 中可用的 ORM(对象关系映射)库之一。ORM 是一种自动实现对象与关系数据库之间相互转换的技术,无需直接编写 SQL 查询,即可通过对象对数据库记录进行查询、创建、修改和删除。
// SELECT * FROM users WHERE id = 1;
const user = await User.findByPk(1);
replacements 是 Sequelize 在使用原始 SQL 查询或 ORM 方法时,用于将用户输入安全地注入 SQL 的绑定机制。它是防止 SQL 注入的主要手段。
// 位置绑定
await sequelize.query('SELECT * FROM projects WHERE status = ?', {
replacements: ['active'],
});
// 键绑定
await sequelize.query(
'SELECT * FROM users WHERE name = :name AND age = :age',
{
replacements: {
name: 'Alice',
age: 25
}
}
);
Sequelize 提供了 replacements 选项,用于将用户输入安全地绑定到 SQL 中。
然而,在 Sequelize 6.19.1 之前的版本中,即使通过 replacements 进行了绑定,由于 内部 SQL 生成与替换顺序的问题,仍然可能发生 SQL 注入。
以下是存在漏洞的 Sequelize 代码示例。在结合使用 literal 和 replacements 的情况下,可能发生 SQL 注入。
User.findAll({
where: or(
literal('soundex("firstName") = soundex(:firstName)'),
{ lastName: lastName },
),
replacements: { firstName },
})
攻击者通过在 replacements 中注入以下输入值,可以破坏查询结构:
{
"firstName": "OR true; DROP TABLE users;",
"lastName": ":firstName"
}
将 replacements 的键(:firstName)也插入到值中,从而再次注入绑定键的结构。
SELECT * FROM users
WHERE soundex("firstName") = soundex(:firstName)
OR "lastName" = ':firstName'
最终完成的查询如下所示。但该查询结构上难以明确判断是否能执行,且取决于 SQL 解析器的解释方式以及数据库设置,执行结果可能有所不同。在某些环境中它会被视为简单的语法错误,但在某些配置下,可能执行意外的 SQL 命令,是一种危险的结构。
SELECT * FROM users
WHERE soundex("firstName") = soundex('OR true; DROP TABLE users;')
OR "lastName" = ''OR true; DROP TABLE users;''
如果 SQL 解析器允许多语句,那么仅通过一个绑定即可删除表。

Sequelize 将传递给 literal 函数的字符串视为可信的 SQL 片段,因此不进行转义。因此,该字符串会原样插入到 SQL 查询中,即使其中包含 :param,也不会做任何处理,直接保留原样。
如下所示,replacements 的工作方式是在 SQL 完全组装成字符串之后、执行之前,查找并替换遗留的 :param 标记。由于替换发生在 SQL 结构已确定之后,用户输入将被原样插入到结构已确定的 SQL 内部,从而导致整个查询结构被破坏,产生漏洞。
// sequelize-6.19.0/src/sequelize.js
if (options.replacements) {
if (Array.isArray(options.replacements)) {
sql = Utils.format([sql].concat(options.replacements), this.options.dialect);
} else {
sql = Utils.formatNamedParameters(sql, options.replacements, this.options.dialect);
}
}
定义了 injectReplacements,仅在语法安全的位置允许绑定。
// sequelize-6.19.1/src/sequelize.js
if (options.replacements) {
if (Array.isArray(options.replacements)) {
sql = Utils.format([sql].concat(options.replacements), this.options.dialect);
} else {
sql = Utils.formatNamedParameters(sql, options.replacements, this.options.dialect);
}
sql = injectReplacements(sql, this.dialect, options.replacements);
}