node防范sql注入// 写法1字符串拼接 util.format const sql util.format(SELECT * FROM someTable WHERE id %s and name %s, req.params.id, req.params.name); connection.query(sql, function (err, results) {}) // 写法2占位符 ? 参数数组预编译参数化查询 const sql2 SELECT * FROM someTable WHERE id ? and name ?; connection.query(sql2, [req.params.id, req.params.name], function (err, results) {})写法 1util.format❌ 有 SQL 注入漏洞util.format只是普通 JS 字符串格式化替换只是简单把变量文本直接拼进 SQL 字符串不会做数据库转义。攻击示例 假设req.params.name or 11 拼接之后最终 SQL 变成SELECT * FROM someTable WHERE id 1 and name or 11条件恒成立攻击者可以查询全部数据还可以执行;drop table xxx;等恶意语句。⚠️ 注意%s只是 JS 层面字符串替换不是 mysql 的转义函数写法 2? 占位符 参数数组✅ 参数化查询防注入mysql/mysql2 库内部会把 SQL 模板和参数分开交给 MySQL 服务端SQL 模板SELECT * FROM someTable WHERE id ? and name ?先发给 mysql参数数组[id,name]作为独立数据传递不会把参数直接拼到 SQL 语句文本里MySQL 服务端做解析参数永远被当做普通数据值不会被解析成 SQL 语法。即使传入 or 11它只会被当做一个普通字符串值不会破坏 SQL 语法从根源防止 SQL 注入。核心根源把外部可控的用户输入直接拼接到 SQL 语句字符串中。 用户输入url 参数req.params、查询参数req.query、post 请求体req.body、cookie、header这些都是外部可控不可信任。✅安全原则不要在 JS 层拼接 SQL 文本使用占位符?参数化查询把输入当数据不当 SQL 语法。1、模板字符串${}拼接高频踩坑❌不安全代码// 危险模板字符串直接插值 const name req.body.name; const sql SELECT * FROM user WHERE name ${name}; connection.query(sql, (err, res){});不安全原因JS 层面直接把用户变量拼进 SQL 字符串。如果name值为 or 11就会 SQL 注入篡改查询条件。✅纠正使用占位符参数放数组const sql SELECT * FROM user WHERE name ?; connection.query(sql, [req.body.name], (err, res){});2、字符串 加号拼接❌不安全const id req.params.id; const sql delete from user where id id; connection.query(sql, callback);原因用户传入id1 or 11会删除整张表全部数据。✅纠正const sql delete from user where id ?; connection.query(sql, [id], callback);3、util.format/sprintf‑like 格式化工具拼接 SQL刚才你例子❌不安全const sql util.format(select * from user where name%s, req.query.name); connection.query(sql, callback);util.format只是 JS 字符串替换不是数据库转义函数不会处理单引号特殊字符。✅纠正放弃 util.format 拼接 SQL使用?占位符。4、数组 join 拼接动态条件很多人做多条件查询踩坑❌不安全let conds []; if(req.query.name){ conds.push(name${req.query.name}); // 这里直接拼接输入 } if(req.query.status){ conds.push(status${req.query.status}); } const sql select * from user where conds.join( AND ); connection.query(sql, callback);原因conds内部直接拼接用户输入存在注入。✅正确做法条件数组 参数数组分开维护let conds []; let params []; // 专门存放参数 if(req.query.name){ conds.push(name ?); params.push(req.query.name); } if(req.query.status){ conds.push(status ?); params.push(req.query.status); } let sql select * from user; if(conds.length 0){ sql WHERE conds.join( AND ); } connection.query(sql, params, callback);5、直接把用户输入当做表名 / 字段名⚠️重点?占位符不能用于表名、字段名只能用于值❌不安全// 用户传入排序字段直接拼接 const sortField req.query.sortField; const sql select * from user ORDER BY ${sortField}; connection.query(sql,[] , callback);?不能写表 / 字段下面这样写没有效果语法错误const sql select * from user ORDER BY ?; // ❌错误?只代表值不能代表列名不安全原因用户输入字段名 / 表名不受占位符保护可以注入。 攻击sortField id;drop table user;-- ✅纠正白名单校验不直接使用用户输入只允许预设好的字段。// 白名单允许的排序字段 const allowFields [user_id,name,create_time]; let sortField user_id; // 默认 if(allowFields.includes(req.query.sortField)){ sortField req.query.sortField; } const sql select * from user ORDER BY ${sortField}; connection.query(sql, [], callback);这里虽然用了${}但是变量来自白名单不是直接外部输入是安全。6、exec /query 直接接收完整用户输入 SQL极度危险❌不安全// 完全接收用户传过来的SQL语句千万不要写 let sql req.body.sql; connection.query(sql, callback);原因用户可以执行任意 SQL删库、查所有数据。 ✅纠正业务禁止接收完整 SQL 字符串只能写固定模板参数使用占位符。7、手动写 replace 简单过滤单引号企图防注入自以为安全实际不安全❌不安全// 错误自己简单替换单引号不能防御全部注入场景 let name req.query.name.replace(//g,); let sql select * from user where name${name};原因数据库的转义逻辑很复杂要处理\、反斜杠、双字节字符等手写正则永远有漏洞。 ✅纠正不要自己写过滤交给驱动的参数化?处理。安全 不安全总览表表格写法示例风险等级原因修复方案模板字符串插值select * from t where name${x}高危外部输入拼入 SQL 文本?占位符 参数数组加号字符串拼接select * from t where idx高危输入直接拼接 SQL 文本?占位符 参数数组util.format/sprintf 拼接 SQLutil.format(select * where name%s,x)高危只是 JS 字符串替换不做数据库转义?占位符 参数数组join 拼接条件内部插值conds.push(name${x})高危条件片段拼接用户输入分开维护条件数组、参数数组用户输入作为字段 / 表名直接插值order by ${req.query.sort}中危?不支持字段名可注入白名单过滤校验直接接收完整 SQL 语句执行connection.query(req.body.sql)极高危可执行任意 SQL禁止使用固定 SQL 模板手写 replace 过滤单引号x.replace(//g,)高危无法覆盖全部转义场景放弃手写过滤用参数化查询重要两个知识点?占位符只能用来替换【值】不能替换表名、字段名、关键字。 表名、字段名需要用白名单机制校验。参数化查询不是简单的字符串替换驱动把 SQL 模板和参数分开传给 MySQL 服务端参数永远被当做数据不会被解析为 SQL 语法。快速自检口诀用户输入是否直接出现在 SQL 字符串模板里面是 → 大概率不安全用户输入全部放到第二个参数数组 → 安全。拓展ORM 框架Sequelize、TypeORM、MyBatis‑Plus 底层已经封装参数化查询。 但是如果你在 ORM 里面手写raw原生 SQL 时依然会遇到注入风险同样要遵守上面规则。举个 Sequelize 危险例子// ❌raw模式直接拼接输入注入风险 const res await sequelize.query(select * from user where name${req.query.name}) // ✅raw模式参数化 const res await sequelize.query(select * from user where name ? ,{ replacements:[req.query.name] })SQL 注入常见攻击 Payload 示例环境node‑mysql后端直接拼接用户输入没有参数化查询。 注意这些仅用于学习理解原理禁止用于未授权的真实系统前置说明假设接口/getUser?namexxx后端危险代码漏洞版// ❌漏洞代码字符串拼接 const name req.query.name; const sql SELECT * FROM sys_user WHERE name ${name}; connection.query(sql, callback);下面演示不同恶意name参数传入后拼接出来的最终 SQL 和效果。1、条件绕过查询全部数据Payloadname OR 11URL 编码后浏览器传参name%27%20OR%20%271%27%3D%271拼接后 SQLSELECT * FROM sys_user WHERE name OR 11逻辑11永远 true返回 sys_user 表所有用户数据。带注释版本把后面单引号注释掉更常用 Payloadname OR 11 ----是 mysql 注释符号--后面必须有空格后面语句全部注释。 拼接 SQLSELECT * FROM sys_user WHERE name OR 11 -- -- 将原来 SQL 末尾的单引号注释语法不出错。2、联合查询 union select偷其他表数据Payloadname UNION SELECT user_id,user_name,password,status FROM sys_user --拼接后SELECT * FROM sys_user WHERE name UNION SELECT user_id,user_name,password,status FROM sys_user -- 效果利用union联合查询爆出账号密码哈希。union 要求前后查询的列数量一致攻击者会先猜列数。3、删除数据高危Payloadname; DELETE FROM sys_user; --拼接 SQLSELECT * FROM sys_user WHERE name ; DELETE FROM sys_user; -- 分号;结束第一条 select开启新 SQL 语句执行 delete清空用户表。 ⚠️注意mysql 驱动默认multipleStatements是关闭多语句会被拦截如果开启了多语句这个攻击生效。 配置项{multipleStatements: true}千万不要随便开。4、更新篡改数据Payloadname; UPDATE sys_user SET status1 WHERE user_id1; --拼接SELECT * FROM sys_user WHERE name ; UPDATE sys_user SET status1 WHERE user_id1; -- 把管理员账号设置为停用。5、报错注入拿库名表名Payloadname AND updatexml(1,concat(0x7e,database()),1) --利用 mysql 报错函数数据库抛出异常时把当前数据库名称暴露在错误信息里。 拿到库名之后继续爆所有表、字段。6、针对排序 order by 注入前面讲的字段直接拼接漏洞接口/list?sortxxx漏洞后端代码const sort req.query.sort; const sql select * from sys_user order by ${sort};Payloadsortid;drop table sys_role;--拼接 SQLselect * from sys_user order by id;drop table sys_role;--直接删除角色表。这种场景?占位符无效只能白名单校验修复。7、绕过简单过滤的 payload有些开发者简单把替换为空name.replace(//g,)原始输入name OR 11替换掉单引号之后变成name OR 11依然可以注入。证明手写正则替换字符串防御 SQL 注入几乎是不可能的总会被绕过。✅同样 payload使用参数化查询会发生什么安全代码const name req.query.name; const sql SELECT * FROM sys_user WHERE name ?; connection.query(sql, [name], callback);传入 payload OR 11 --MySQL 会把整个 payload 全部当成普通字符串的值去匹配 name 字段。 等价逻辑SELECT * FROM sys_user WHERE name \ OR 11 -- 数据库寻找name字段值等于字符串 OR 11 --的记录几乎没有数据攻击失效。驱动自动转义内部的单引号不会把 payload 解析成 SQL 语法。