1. TOCTOU(Time-of-Check to Time-of-Use) #toctou
// 割引クーポン使用処理(脆弱)
1. クーポンが未使用かチェック ← Time of Check
2. 割引額をユーザの注文に適用
3. クーポンを「使用済み」にマーク ← Time of Use
並行で 2 リクエストを送ると:
リクエスト A:[1.チェック → 未使用] [2.適用] [3.使用済みに]
リクエスト B: [1.チェック → 未使用] [2.適用] [3.使用済みに]
↑ A の 3.が完了する前に B のチェックが走る
→ 1枚のクーポンが2回使われる
2. 具体例 #examples
- クーポン・割引コードの二重適用 — 100% 割引クーポンを多重適用
- 残高引き落としの二重実行 — ¥10,000 残高で ¥10,000 注文を並行2発射 → 残高 ¥−10,000 で2件購入
- 1人1個限定セールの突破 — 数量制限を並行リクエストで超過
- ポイント二重消費 / 二重付与
- 退会処理中の権限残存 — 退会フラグが立つ前に管理操作を実行
3. Single-Packet Attack #single-packet
近年の Race Condition 研究で注目される手法。複数の HTTP リクエストを同一の TCP パケットに詰めてサーバに送ることで、ネットワーク遅延の影響を排除し、サーバ側の同時処理タイミングを揃える。HTTP/2 のマルチプレキシングを使うと簡単に実現可能(Burp Suite の Turbo Intruder で実装)。
4. 正しい防御 #defense
- DB トランザクション + 行ロック —
SELECT ... FOR UPDATEで「チェックと更新」を不可分に - UNIQUE 制約 — 同じユーザによるクーポン使用を一意に。重複は DB レベルで弾く
- 楽観ロック(version カラム) — UPDATE 時に
WHERE version = ?を付け、版が変わっていれば失敗 - 分散ロック — Redis SETNX、ZooKeeper 等で「同じリソースに対する処理は一度に1つ」を保証
- Idempotency Key — クライアントが提供する一意の ID を保存して、同じ ID の処理は1回のみ実行(Stripe 等の決済 API のパターン)
// PostgreSQL - 行ロック BEGIN; SELECT * FROM coupons WHERE code = 'SUMMER25' AND used = false FOR UPDATE; -- 他のトランザクションはこの行を読めない UPDATE coupons SET used = true, used_by = $user_id WHERE id = $id; COMMIT;