Buffalo框架路由层不防重放,因app.GET/POST仅做路径匹配与handler绑定,不解析body、不校验请求内容;重放攻击发生在路由执行完毕后的handler阶段,故必须在中间件中手动实现timestamp+sign校验。

Buffalo 框架路由层不防重放,必须在中间件里手动加 timestamp + sign 校验,否则任何带 body 的 POST/PUT 请求都可无限重放。
为什么不能只靠路由注册防重放
Buffalo 的 app.GET、app.POST 只做路径匹配和 handler 绑定,不介入请求内容校验。它甚至不自动解析 Request.Body,更不会拦截或改写 header。所谓“路由防重放”是误解——重放攻击发生在请求已抵达你的 handler 之后,而路由早已执行完毕。
- 攻击者可截获一次合法的
POST /api/pay请求,反复发送,只要 signature 和 timestamp 不过期,服务端就会重复扣款 - Buffalo 的
buffalo generate resource生成的 CRUD 路由默认不带任何签名逻辑,连X-Timestamp头都不读 - 你无法通过修改
routes/app.go中的app.POST参数来启用防重放——那些参数只控制路径、中间件顺序,不触发校验
怎么在中间件里加 timestamp + sign 校验
核心是:从 header 提取 X-Timestamp 和 X-Signature,用相同密钥和算法重算 sign,再比对时间差。关键点不是“写了中间件”,而是“写对了顺序和细节”。
- 必须从
Header读X-Timestamp,不能从 query 或 body —— 否则攻击者可在签名后篡改 timestamp 并重发 - sign 计算必须包含
timestamp+body(或排序后的 query)+ 密钥,且算法要和客户端严格一致(如HMAC-SHA256) - 校验时间差要用
time.Now().UTC().Unix(),避免本地时区导致误拒;建议阈值设为 300 秒,超时直接c.Error(401, ...) - Body 需可重复读:Buffalo 默认把
req.Body当作单次流,中间件里ioutil.ReadAll一次后,后续 handler 就拿不到数据了,得用c.Request().WithContext(...)或提前缓存并重置req.Body
nonce 去重为什么不能用数据库唯一约束
用 MySQL/PostgreSQL 主键唯一索引做 nonce 去重,看似简单,实则高危:并发插入冲突会抛 unique_violation 错,错误处理不统一就可能漏掉重放;更严重的是存在竞争窗口(Race Window),两个请求几乎同时查不到 nonce、又几乎同时插入成功。
- 单机部署可用
sync.Map+ 定时清理,但要注意内存泄漏风险(key 不主动删,map 不释放) - 生产环境强烈推荐 Redis:
SET idempotent:nonce:,原子写入,失败即说明已存在appId:nonce"1" EX 300 NX - 千万别复用
authenticity_token:它是 session 绑定、单次 CSRF Token,不校验 body 完整性,也不含时间戳,攻击者 GET 一次就能拿到并重放任意 POST
真正难的不是代码,是时钟与存储一致性
哪怕你中间件逻辑完全正确,只要服务器之间 UTC 时间差超过 300 秒,或者 Redis 网络抖动导致 SET NX 返回失败但业务没降级策略,合法请求就会被误杀。这些不是“写完就跑”的问题,而是上线前必须压测、监控、配置熔断的真实瓶颈。











