参数化查询是唯一可靠防SQL注入方案,必须用驱动支持的占位符(如mysql2用?配数组、pg用$1)绑定值;表名/字段名等结构部分须白名单校验后硬编码,不可参数化。

只要接口里有数据库查询,且用了用户输入拼SQL字符串,就一定存在SQL注入风险——参数化查询是唯一可靠解法,其他校验、转义、过滤都只是辅助。
mysql2/pg/node-mssql 的参数化怎么写才真正生效
占位符必须配合对应驱动的参数传递机制,否则只是普通字符串。常见失效场景:
-
mysql2.query('SELECT * FROM users WHERE id = ?', req.query.id)—— 错:第二个参数必须是数组,[req.query.id]才触发绑定 -
pg.query('SELECT * FROM logs WHERE level = ?', ['error'])—— 错:pg 不认?,得用$1,且顺序严格匹配 -
node-mssql用@param时漏掉ps.prepare()或传参类型不匹配(比如sql.Int却传字符串),参数不会绑定 - 把
"SELECT * FROM ? WHERE id = ?"当成安全写法——表名不能参数化,?在这里根本不起作用
Sequelize 中 literal() 是个雷,一碰就炸
Sequelize.literal() 的作用就是绕过所有防护,把字符串原样塞进 SQL。它只该出现在完全静态、无任何变量参与的场景里:
- ✅ 安全:
attributes: [[literal('NOW()'), 'ts']]、order: [[literal('id DESC')]] - ❌ 危险:
where: { name: { [Op.like]: literal(`%${req.query.q}%`) } }—— 用户输入直接进 literal,等同于裸拼 - ❌ 危险:
attributes: [[literal(req.query.calc), 'result']]—— 动态表达式可能含UNION SELECT或子查询 - ⚠️ 注意:
Sequelize.fn('LOWER', Sequelize.col('name'))比literal()安全得多,优先用内置函数
动态字段(排序、表名、列名)必须白名单硬控
Joi 或 express-validator 只能校验值,对 SQL 结构无效。这些地方没法参数化,只能靠代码层拦截:
- 排序字段:
const allowedSort = ['id', 'name', 'created_at']; if (!allowedSort.includes(req.query.sort)) throw 400; - 表名拼接:
const tableMap = { user: 'users', order: 'orders' }; const tableName = tableMap[req.query.type] || 'users'; - 字段列表:
const allowedFields = new Set(['id', 'email', 'status']); const fields = req.query.fields?.split(',') || []; if (fields.some(f => !allowedFields.has(f))) throw 400; - 配置文件里存 SQL 模板?绝对不行。JSON/YAML/JS 配置一旦含
${}或拼接逻辑,加载即执行,RCE 和注入风险并存
最难防的不是主查询路径,而是日志、错误回显、监控埋点里悄悄拼的 SQL 字符串——哪怕只有一处漏掉参数化,整个防御就形同虚设。


















