OceanAltOceanAlt
合規與安全2026-07-208 分鐘讀完

攻防實驗室 #5:一張授權我付了兩次 + 把審計指紋推到我們自己也改不了的地方

兩處一起打。其一:支付沒有一次性 nonce,一筆通過全部閘的合法支付可被原樣重放到刷幹額度——我們補上防重放閘(重放→409)。其二:第4期的 hash 鏈只活在我們自己的庫裡,管理員重寫整條尾鏈無人能證偽——我們把鏈尾指紋公開(/api/audit-anchor)並讓 GitHub 定時外部見證。誠實邊界:真正不可抵賴還差把指紋錨上公鏈。

OOceanAlt 編輯部
分享:XLinkedInFacebookTelegramWhatsApp微博🖼 海報(英文)

攻防實驗室 · 第 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 鏈:刪或改任意一條都會斷鏈、可被校驗發現。但我們當時承認了一個前提:要神不知鬼不覺,攻擊者得把這條之後的每一條全部重算重寫;而能證明"重寫前的指紋是什麼"的地方,只有我們自己的數據庫。

同一個信任域裡的證據,不算證據。 如果我們把整本賬都重寫了,又是我們自己說"這就是原來的樣子"——那和沒有防線區別不大。

補:把指紋錨定到我們自己也控制不了的地方

我們做了兩件事:

  1. 公開指紋端點 GET /api/audit-anchor:返回當前 hash 鏈的鏈尾指紋(tip)。任何人——監控、投資人、路人——都能定期記下它。
  2. 獨立系統定時見證:一個 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 攻防實驗室 · 研究筆記)

分享:XLinkedInFacebookTelegramWhatsApp微博🖼 海報(英文)

這篇內容的出處與狀態

署名
OceanAlt 編輯部
首次發佈
2026-07-20
最後更新
2026-08-14
原始資料
未標註來源

引用這篇內容

OceanAlt 編輯部(2026)。《攻防實驗室 #5:一張授權我付了兩次 + 把審計指紋推到我們自己也改不了的地方》。OceanAlt。https://oceanalt.com/zh/articles/attack-lab-5-replay-and-anchoring(訪問日期:2026-09-16)

本文遵循 編輯準則與事實核查標準。發現錯誤請告訴我們,核實後會在此處公開更正。

先試一下 · 免費、不用註冊

付款前把收款地址粘進去,看它有沒有上過制裁名單、混幣器或詐騙標籤。

這套判斷也可以放進你自己的產品

一行代碼,不碰你的樣式和 JS。同一套結算前風險判斷可以出現在你的文章、你的錢包確認頁,或者作為接口供你的 Agent 在付款前調用。

組件不收集讀者身份。接入不代表 OceanAlt 對你的產品或頁面上的地址作安全背書。