fix(payments): Phase 3 — 二重決済・受注消失・ガス濫用・x402 取りこぼしを塞ぐ - #273
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
全コードベースレビュー第 4 回 (PR #272) で保留した Phase 3 の 5 系統を、価格・料率を一切 変えずに修正した。**実装後に敵対レビューを 2 巡かけ、新規コードの欠陥 9 件を摘出して修正済み**。 ## 1. standard 経路で手数料を拒否すると受注が店に届かない [HIGH] mode=standard は拒否しない (SA 非対応/pristine EOA の正規フォールバックのため)。代わりに merchant leg 確定時点で受注を通知し、手数料未収を記録。fee 成立後は after() で payer・順序・ 金額を検証し単一 Lua transaction で claim と同時に解除する。 ## 2. リロードによる二重決済 [HIGH ×2] EIP-3009 署名は永続化しない。sessionStorage に持つのは chainId/from/merchant/金額/nonce/ validBefore/経路種別のみ。mount 時に read-only status へ問い合わせ、成立済みは復元・不明は 再署名を封鎖・validBefore 超過かつ未使用なら破棄。standard は broadcast 済み txHash の receipt を再照会。 ## 3. feeKind 濫用によるガス持ち出しと共有日次予算の枯渇 [MEDIUM] 料率・フロアは変えず、「回収額がガスフロア未満の settle」専用の日次枠と payer 枠を追加。 閾値は caller 別 (x402 は x402 自身の fee floor・recover/mobile は relayGasFeeValue) で、 x402 の 1〜5 JPYC リソースが sub-floor 扱いになる誤判定を防ぐ。 新 env: RELAY_SUBFLOOR_DAILY_TX_CAP / RELAY_SUBFLOOR_PAYER_DAILY_TX_CAP (掟9 同期済み)。 ## 4. x402 の取りこぼしと回復経路 [MEDIUM ×2] first-party 有料 API に再課金しない再配信 (所有トークン付き claim・broadcast 前の拒否でのみ解放)。 エージェント注文に read-only status での回復。settled 判定は forwarder が発火した Settled event の 6 フィールド (from/nonce/merchant/merchantValue/feeReceiver/feeValue) 完全一致を必須とし、 同一 receipt 内の別 settle への誤帰属を防ぐ。 ## 5. FX 換算 QR の 3 分期限 [正直な注記のみ] QR パラメータは未署名で支払者が敵対者である以上サーバ検証は不可能。**検証したように見えるだけの ガードは足さず**、UI 上の目安である旨をコメントに明記するに留めた。 ## 敵対レビューで摘出し修正した新規欠陥 (抜粋) - 1 回の支払いで多数の resource を解錠できる誤帰属 (Settled event 全フィールド照合で解消) - 追加した sub-floor ガード自体が x402 settle を当日全停止させる DoS (閾値の caller 別化で解消) - 同一 receipt 内 fee の二重充当 (claim と保存を単一 Lua transaction 化) - broadcast 前の拒否で pending claim が残り正規決済が再試行不能 (所有トークン CAS で解放) - 店舗負担 fee が 1 最小単位差で恒久未収表示 (逆算の 2 候補を受理・顧客負担は exact 維持) - 日跨ぎ refund が翌日 budget を負数化 (INCR したキーを token として返し同一キーのみ DECR) - 過剰補正で複数 settle batch が永久 indeterminate (receipt 合算比較を撤去) ## 既知の受容 (user 裁定 2026-07-26) レジ手数料の用途 claim は公開 endpoint を先回りされると正規注文の未収バッジを固定化できる。 資金は動かず影響は会計表示のみのため現状で受容。完全に閉じるにはレジ専用 feeReceiver の分離か サーバ発行の一回限り intent が必要 (実利用が出たら再検討)。 ## 検証 typecheck / **full vitest 8030 passed (391 files)** / next build + bundle 予算 全ルート内 / npm run lint (新規警告ゼロ) / audit-gate / lockfile-gate すべて PASS 設計裁定・統合監査 = Claude Fable 5、実装 = Codex 5.6 Sol (ultra/xhigh) と Claude Opus 5、 敵対レビュー = Codex 5.6 Sol。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CNMEwurCJNTrzvr2qypSd
cipherwebllc
added a commit
that referenced
this pull request
Jul 27, 2026
本番実測 (2026-07-27) で `/api/paid/*` の応答が Next.js 既定の `cache-control: public, max-age=0, must-revalidate` を返しており、**共有キャッシュ (CDN / プロキシ) への保存が許可**されていた。402 は支払い条件 (payTo / 金額 / forwarder / commitVersion) を、200 は購入者だけが受け取るべき有料コンテンツを含む。 `Vary` に X-PAYMENT が無いため、URL だけを鍵にしたキャッシュが別の買い手へ再配信し得る。 ## 変更 - `handleFirstPartyPaidGet` の出口で `Cache-Control: no-store` を一律に付与。 分岐が 20 以上あり個々の return に付けると新規分岐で漏れるため、境界 1 箇所で保証する (掟13: 何の波及を断つ防御かをコメントで明記)。 - 402 と 200 の双方で no-store を固定するテストを追加。 契機は x402 上流の PR (402 応答へ Cache-Control: no-store を足す議論) と、 主要 Facilitator 15 件の安全規則違反を報告した査読前研究 (arXiv 2026-07-21)。 研究が挙げる他 5 系統 (settle 前の状態再検証 / ガス消耗 / settle 失敗の成功扱い / 支払い後の未配信 / 不明状態の成功扱い) は PR #273 で対応済みのため本 PR では触れない。 ## 検証 typecheck / **full vitest 8036 passed (392 files)** / next build + bundle 予算 全ルート内 / eslint / audit-gate / lockfile-gate PASS。 Claude-Session: https://claude.ai/code/session_015CNMEwurCJNTrzvr2qypSd Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
全コードベースレビュー第 4 回(PR #272)で仕様判断が必要として保留した Phase 3 の 5 系統を、価格・料率を一切変えずに修正しました。設計方針は Fable 5 が裁定し、実装は Codex 5.6 Sol (ultra)、監査は Fable 5 です。
修正内容と、設計上の判断
1. standard 経路で手数料を拒否すると受注が店に届かない [HIGH]
モバイル注文 ×
mode=standardは店舗送金の後に独立した手数料 tx を要求するため、顧客が 2 本目を拒否すると受注 webhook が発火せず、顧客は払ったのに注文が店に届かない状態になっていました。mode=standardは拒否していません。switchToStandardは SA 非対応 / pristine EOA 時の正規フォールバック導線(3 箇所から到達)で、拒否すると gasless できないウォレットの顧客が支払い不能になるためです。代わりに merchant leg が確定した時点で受注を通知し、手数料未収を記録します。fee tx 成立後はafter()で payer・順序・金額を検証して CAS で解除。両 tx 成功の従来ケースは応答・完了画面とも不変です。2. リロードによる二重決済 [HIGH ×2]
EIP-3009 の署名は永続化していません。 計画(
plans/relay-unknown-autoresolve.md)の v2 案は署名済み payload の退避でしたが、保存の是非が未裁定であるため採用せず、秘密でない intent メタデータのみ(chainId / from / merchant / 金額 / nonce / validBefore / 経路種別)を sessionStorage に保存します。mount 時に既存の read-only status route へ問い合わせ、成立済みは復元・不明は再署名を封鎖・validBefore 超過かつ未使用なら破棄します。3. feeKind 濫用によるガス持ち出しと共有日次予算の枯渇 [MEDIUM]
料率・フロアは変えていません(フロア追加は小口モバイル注文の実質値上げとなり、掟14 の開示 3 点セット同期が必要な価格変更になるため)。代わりに「回収額がガスフロア未満の settle」専用の日次枠と payer 単位枠を additive に追加しました。正規の小口注文は従来どおり通り、攻撃者は共有予算に達する前に別枠で止まります。判定は client 申告の
feeKindではなくサーバ算出済みの回収額とチェーン別ガスフロアの比較で行います。新 env 2 件は
.env.local.exampleと README の env 表を同時更新済みです(掟9)。4. x402 の取りこぼしと回復経路 [MEDIUM ×2]
first-party 有料 API に再課金しない再配信(支払い identity に束縛・resource/query/scope/credential を照合して replay を拒否)を、エージェント注文に relay と同型の read-only status による回復を追加しました。新規の公開 route
/api/facilitator/statusが増えます(read-only・IP レート制限あり・既存応答は不変)。5. FX 換算 QR の 3 分期限 [正直な注記のみ]
QR パラメータは店舗発行ですが未署名で、支払者が敵対者である以上サーバでは検証できません。実効的な修正には QR 全体の署名が必要で本件の範囲外です。「検証したように見えるだけ」のガードは足さず、UI 上の目安である旨をコードコメントに明記するに留めました(今回のレビューで排除した LARP を自分で作らないため)。
検証(統合ブランチで実施)
npm run typecheck— PASSnpx vitest run— 7982 passed / 388 filesnpm run build+check-bundle-budget.mjs— 全ルート予算内(/pay 436/436・/tip 432/434)npm run lint— PASS(新規警告ゼロ)/audit-gate/lockfile-gate— PASSgrep signature|privateKey= 0)・料率定数の変更なし・FX に検証を装うコード無し🤖 Generated with Claude Code
https://claude.ai/code/session_015CNMEwurCJNTrzvr2qypSd