Node.js层必须用参数化查询且禁用字符串拼接,ORM原生SQL接口如sequelize.query若拼接用户输入即失效;动态表名/字段名须白名单校验,所有用户输入(含query、params、headers)均需绑定参数或白名单控制。

Node.js层必须用参数化查询,不是“用了ORM就安全了”
ORM默认走参数化,但所有原生SQL接口(sequelize.query、prisma.$queryRaw、pool.query)一旦用字符串拼接,立刻失效。安全不是靠框架背书,而是看每一行查询是否把用户输入当“值”而非“代码”处理。
-
sequelize.query('SELECT * FROM users WHERE id = ?', [req.query.id])✅ -
prisma.$queryRaw`SELECT * FROM users WHERE name = ${req.body.name}`❌ 模板字符串插值=拼接 -
pool.execute('UPDATE posts SET title = ? WHERE id = ?', [title, id])✅ -
db.query(`DELETE FROM logs WHERE user = '${req.user}'`)❌ 即使加了mysql.escape()也不可靠,escape只适配MySQL且易被宽字节绕过
动态表名/列名/ORDER BY字段必须走白名单,不能“过滤后拼接”
占位符?只能用于值,不能用于SQL结构。攻击者一旦能控制ORDER BY字段或FROM表名,就能绕过所有参数化保护。
- 错误做法:
const sql = `SELECT * FROM ${req.query.table} WHERE id = ?` - 正确做法:定义硬编码白名单,如
const allowedTables = { users: 'users', posts: 'posts' },再取allowedTables[req.query.table] || 'users' - 排序字段同理:
const sortFields = { name: 'name', createdAt: 'created_at' },然后拼接ORDER BY ${sortFields[req.query.sort] || 'id'} - 别信正则过滤单引号、分号——反引号、
%27、0x27都能绕过
所有用户输入入口都得进参数化流程,不只是req.body
开发者常只盯req.body,却忽略req.query、req.params、req.headers甚至req.ip——只要这些进了SQL,就必须绑定参数。
-
req.query.id用于分页查询?→pool.execute('SELECT * FROM items LIMIT ?, ?', [offset, limit]) -
req.headers['x-tenant-id']用于多租户隔离?→ 必须走白名单校验后拼接,或作为参数传入WHERE条件 -
req.params.slug查文章?→pool.execute('SELECT * FROM posts WHERE slug = ?', [req.params.slug]) - Controller层
parseInt(req.query.id)只是fail-fast,不是安全动作;若后续仍拼接SQL,照样危险
前端Vue校验对SQL注入零防护力,但能减少无效请求
Vue里用v-model限制输入长度、zod校验schema、甚至MD5加密密码——全被curl或Burp一发原始请求绕过。前端唯一能做的,是让恶意请求更难构造、更快失败。
立即学习“前端免费学习笔记(深入)”;
- 在表单提交前过滤
<script>、javascript:等XSS特征(防的是XSS,不是SQL注入) - 对ID类字段做
Number.isInteger拦截明显非法值,避免后端日志刷屏 - 不依赖正则黑名单防注入,比如
/['";\-]/g——攻击者用UNION/**/SELECT或十六进制编码轻松绕过 - 真正关键的防线只有一处:数据库操作层是否100%未拼接任何
req.xxx到SQL字符串中
真正容易被忽略的点是:表名、列名、GROUP BY字段这些动态SQL结构,既不能参数化,也不能靠“转义”解决,必须靠白名单硬编码控制;而所有进SQL的用户输入,无论来源,都要统一走参数绑定或白名单校验——没例外,不妥协。


















