攻防实验室 #5:一张授权我付了两次 + 把审计指纹推到我们自己也改不了的地方
两处一起打。其一:支付没有一次性 nonce,一笔通过全部闸的合法支付可被原样重放到刷干额度——我们补上防重放闸(重放→409)。其二:第4期的 hash 链只活在我们自己的库里,管理员重写整条尾链无人能证伪——我们把链尾指纹公开(/api/audit-anchor)并让 GitHub 定时外部见证。诚实边界:真正不可抵赖还差把指纹锚上公链。
攻防实验室 · 第 5 期。上期结尾我们诚实交代了一个没堵死的洞:hash 链能发现"删改",但一个管理员如果重写整条尾链,唯一能证明"原来的指纹长什么样"的地方——还是我们自己的数据库。这期把这个洞堵上。顺手,我们又打穿了一处:重放。
攻击一:一张签过的授权,我付两次
网关有八道闸,一笔合法支付会依次通过:归因、身份证明、单笔额度、单日累计、白名单、意图比对、AML……全过,结算。看起来滴水不漏。
问题是——它从没检查过:这是不是同一笔请求的第二次。
真实运行结果(线上网关,修复前):
1) agent 发起一笔合法支付(0.05 USDC,收款在白名单,用途正确)
→ 200 allow,结算
2) 把上面那个请求原样再发一次(一字不改)
→ 200 allow,又结算了一次
3) 循环 N 次(只要不超单日累计上限)
→ 一张授权,付了 N 次
每一道闸都说"放行"——因为它本来就是一笔合法请求。单日累计上限是唯一的兜底,但在上限之内,一次授权可以被复制执行到把额度刷干。
现实世界的类比:一张没有编号的支票,复印几张,每张都能兑现。或者刷卡时没有幂等键,网络一抖,同一笔被扣两次。
补:一次性 nonce(防重放/防双花)
改法很小:每笔支付必须带一个唯一的一次性 nonce,结算前消费一次。
- 第一次见到这个 nonce → 记下、放行、结算。
- 再见到同一个 nonce → 这是重放,拦于
Mandate/Replay(409)。 - 两个一模一样的请求同时到达?数据库的唯一约束保证只有一个能写进去,另一个当场被拒——竞态也堵死。
我们特意把这道闸放在结算前的最后一步:前面七道会全过(因为它确实合法),只有这道能识破"这是第二次"。
修复后,同样的攻击:
| 攻击 | 修复前 | 修复后 | |---|---|---| | 原样重放一笔合法支付 | 🔴 200,再次结算 | ✅ 409 拦于 Mandate/Replay | | 并发重放(同一 nonce 同时打) | 🔴 可能双花 | ✅ 唯一约束保证只结算一次 | | 正常支付(每笔新 nonce) | 200 | ✅ 200(不误伤) |
授权 ≠ 这一次执行是新的。 mandate 授权的是"一类支付"(金额、收款、用途),但每一次执行必须能被唯一标识、且只能用一次——否则一次点头 = 无限次付款。
攻击二:账本指纹只活在我们自己家里(补第 4 期的坑)
第 4 期我们给审计日志上了 hash 链:删或改任意一条都会断链、可被校验发现。但我们当时承认了一个前提:要神不知鬼不觉,攻击者得把这条之后的每一条全部重算重写;而能证明"重写前的指纹是什么"的地方,只有我们自己的数据库。
同一个信任域里的证据,不算证据。 如果我们把整本账都重写了,又是我们自己说"这就是原来的样子"——那和没有防线区别不大。
补:把指纹锚定到我们自己也控制不了的地方
我们做了两件事:
- 公开指纹端点
GET /api/audit-anchor:返回当前 hash 链的链尾指纹(tip)。任何人——监控、投资人、路人——都能定期记下它。 - 独立系统定时见证:一个 GitHub Actions 定时任务,每 4 小时(每天 6 次)抓一次这个指纹,记进 GitHub 的运行记录 + 构件存证。GitHub 是和我们的数据库完全不同的信任域(不同的公司、不同的权限、不同的管理员)。
现在要想神不知鬼不觉地改写审计历史,你得同时攻破两个互相独立的系统:我们的数据库 + GitHub 的记录。而且只要指纹在外部被记录过哪怕一次,重写尾链就会对不上那次记录——藏不住。
这次修复没做到的(照例说清)
GitHub 仍然是一家我们有账号的公司。 真正的"谁都改不了",要把指纹锚定到一条公链上(比如经 OpenTimestamps 上比特币,或走我们已有的 Base Sepolia 结算通道把指纹写进 calldata)。那才是"连我们自己、连 GitHub 都无法抵赖"。我们没假装已经做到——这是下一步。每一层都比上一层更难抵赖,我们在一层层往外推,而不是一步声称做到了终点。
三个教训
1. 幂等,是支付系统的底线,不是加分项。 我们把它当成"以后再说"的优化,而它其实是一个能被反复利用的双花洞。凡是会动钱的接口,第一个该问的问题就是:同一笔来两次会怎样?
2. 能被你自己改的证据,只在别人也改不了时才算数。 防篡改的重点从来不是"锁死",而是"外面有人看着"。没有外部见证的日志,是自己写给自己的情书。
3. 我们连续五期都打穿了自己。 挡住六发、漏了累计、地基是空的、日志能抹掉、授权能重放。这不丢人,不敢公开才丢人。 一个标准值不值得信,不看它宣称多完备,看它被自己人打穿时敢不敢把结果贴出来——包括这两条"我们本可以付两次、本可以改账"。
下一期
我们会把账本指纹推到一条公链上,让"连我们和 GitHub 都无法抵赖";并回头审一遍结算路径本身:真实上链那一档,有没有它自己的重放/对账边界。
亲手打一次:oceanalt.com/firewall · 用你自己的 agent 测:集成指南 · 看指纹:/api/audit-anchor
(OceanAlt 攻防实验室 · 研究笔记)
这篇内容的出处与状态
- 署名
- OceanAlt 编辑部
- 首次发布
- 2026-07-20
- 最后更新
- 2026-08-01
- 原始资料
- 未标注来源

