fastify的onrequest/prehandler hook不适合直接扫描sql关键词,因其仅获原始请求体,无法解析sql语义或判断参数上下文,且手动解析易破坏流式处理、引发syntaxerror,误报率高且易被编码绕过;真正拦截必须下沉至数据库驱动层或dao封装层,通过强制参数化、白名单校验及预编译机制实现。

不能靠 Fastify 自身的 schema 或 hook 做 SQL 注入拦截——它不解析 SQL 语义,只校验 JSON 结构。真正有效的拦截必须发生在数据库驱动层或 DAO 封装层,Fastify 只能做前置粗筛。
为什么 Fastify 的 onRequest / preHandler hook 不适合直接扫 SQL 关键词
Fastify 的生命周期 hook(如 onRequest、preHandler)拿到的是原始请求体,但:
- request.body 在未指定 schema 时是 Buffer 或未解析的字符串,手动 JSON.parse() 会破坏流式处理,还可能抛出 SyntaxError;
- 即使成功解析为对象,你也无法知道哪个字段最终会被拼进 SQL——user.name 可能进 WHERE,user.remark 可能进 INSERT VALUES,而 user.sortBy 可能直接拼进 ORDER BY;
- 对 SELECT * FROM users WHERE id = ${req.query.id} 这种写法,hook 层看到的只是字符串 "1 OR 1=1",但没上下文判断它是否真会被执行为 SQL。
真正该拦截的位置:封装数据库操作的 service 层 + query builder
在 Fastify 微服务中,SQL 注入防御必须下沉到数据访问层,而非框架路由层。推荐做法:
- 所有数据库调用必须走统一的 db.query() 或 db.execute() 封装函数,禁止直接使用 pg.Client#query 或 mysql2#execute;
- 封装函数内部强制检查:
- 对所有
WHERE、HAVING、ORDER BY、LIMIT后的值,只接受预定义白名单(如['id', 'name', 'created_at'])或参数化占位符($1,?),拒绝任何字符串拼接; - 对动态表名/列名,必须查配置映射表:
const realTable = TABLE_MAP[req.query.table],不存在则抛BadRequest; - 启用数据库驱动的 prepare 模式(PostgreSQL 的
prepare: true,MySQL2 的namedPlaceholders: true),让驱动自动剥离参数边界。
async function safeQuery(sql, params) {
// 禁止 WHERE 后出现 ; 或 -- 或 /*
if (/;|--|/*/.test(sql)) throw new Error('SQL syntax not allowed');
// 强制所有值通过 $1, $2 占位
return client.query({ text: sql, values: params });
}
Fastify 层可做的轻量级前置过滤(仅限高危入口)
如果某些接口明确只接受数字 ID 或邮箱格式,可在 preHandler 中加硬约束,比放行后再查库更早止损:
- 对 GET /user/:id 路由,用 params schema 限定 id 为正整数:{ type: 'integer', minimum: 1 };
- 对 POST /search 的 body.keyword,用 body schema 加正则:{ pattern: '^[\w\s\-]{1,50}$' },直接拦掉 ' OR 1=1;
- 避免在 preHandler 中做关键词扫描(如搜 union select),因为:
- JSON 字段值可能被 URL 编码或 Base64 编码,hook 层没解码逻辑;
- 误报率高——业务字段含
select(如产品分类 “Select Edition”)就会被拦; - 性能损耗不可控,每个请求都做全文正则扫描。
最容易被忽略的一点:日志里别打原始 SQL
很多团队在 catch (err) 里记录 err.stack + sql + params,结果把完整注入 payload 写进日志,等于给攻击者送反馈。正确做法:
- 日志中只记 sqlHash(如 sha256(sqlTemplate))和 params.length,不记原始 sql 字符串;
- 错误响应体禁用 stack,统一返回 { code: 'INTERNAL_ERROR', message: 'Request failed' };
- 数据库连接池开启 log: (msg) => logger.debug('[pg] %s', msg) 时,确保 msg 已脱敏。











