参数校验中间件不能直接防御SQL注入,但严格且前置的校验(如Zod/Joi)可拦截95%原始注入尝试;必须配合参数化查询,缺一不可。

参数校验中间件本身不能直接防御SQL注入,但它能切断绝大多数注入入口——前提是校验逻辑足够严格、位置足够靠前。
为什么Zod或Joi校验中间件必须放在数据库操作之前
Express中间件是洋葱模型,顺序错了等于没加。如果先查数据库再校验,攻击载荷已经进SQL执行环节了。常见错误是把校验写在Controller里,但Controller往往已调用Service层的数据库方法。
-
express.json()和express.urlencoded()必须在所有校验中间件之前,否则req.body是 undefined - 校验中间件应挂载在路由级(
router.use())或应用级(app.use()),而不是写在router.get('/xxx', handler)的handler函数内部 - 对
req.query、req.params、req.body的校验必须全覆盖,不能只校验 POST body 而放行 GET 参数
数字型参数不转成 number 就等于没校验
JavaScript 的弱类型会让字符串 "1 OR 1=1" 通过 typeof x === 'number' 检查失败,但若只用 parseInt() 或 Number() 转换,又会把 "1; DROP TABLE users--" 变成 1,看似安全实则埋雷。
- 用
z.string().regex(/^\d+$/).transform(Number)强制纯数字字符串,再配合.int().min(1)确保是合法整数 - 避免
parseInt(req.query.id, 10)这类宽松转换:它会截断"123abc"成123,而攻击者可能故意混入字母绕过简单正则 - 对ID类字段,直接拒绝非数字字符比“清洗后使用”更安全;清洗行为本身可能引入歧义(比如空格处理、Unicode全角数字)
字符串参数必须设白名单字符集和长度上限
SQL注入不止靠单引号,宽字节、注释符、分号、括号、函数名(如 CONVERT、SLEEP)都可能被利用。不限制字符集和长度,等于给攻击者留出构造复杂payload的空间。
- 搜索关键词类字段:用
z.string().max(50).regex(/^[a-zA-Z0-9\u4e00-\u9fa5\s]+$/),禁用所有 SQL 元字符和控制字符 - 邮箱、手机号等有明确格式的字段,优先用
FILTER_VALIDATE_EMAIL类标准验证器,而非自定义正则 - 绝对不要允许
;、--、/*、UNION、SELECT、information_schema出现在任何用户输入中——哪怕是在日志或缓存键里
校验中间件和参数化查询必须组合使用
校验能拦住 95% 的原始注入尝试,但无法替代参数化查询。比如一个动态排序字段 sort=created_at DESC,你不可能用 ? 占位符绑定关键字,这时就得靠白名单校验 + 服务端映射("date" → "created_at DESC")来兜底。
- 所有数据库操作必须使用
mysql2的conn.execute(sql, params)或pg的client.query(sql, params),禁用query(sql + user_input) - ORM 如 Prisma 也要注意:
where: { name: { contains: req.query.q } }是安全的,但prisma.$queryRaw或原生查询仍需手动参数化 - 校验中间件报错时,响应状态码必须是
400或422,不能返回500—— 否则攻击者可通过错误堆栈反推表结构
最易被忽略的一点:校验中间件无法防御那些绕过HTTP层的攻击,比如通过Redis Lua脚本拼接SQL、或从MQ消息体里提取未校验字段再入库。防御边界必须延伸到每个数据入口,而不仅是Express的请求对象。











