utsuroi race labs知識ベース 01 / 01
Race Conditions · Knowledge Base
01

Race Conditions の基礎

When timing becomes a vulnerability

「チェック」と「アクション」の間に時間差があり、その間に状態が変わると、想定外の結果になる。並行リクエストを意図的に送り込むと、本来不可能な操作が可能になる場合がある。

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

3. Single-Packet Attack #single-packet

近年の Race Condition 研究で注目される手法。複数の HTTP リクエストを同一の TCP パケットに詰めてサーバに送ることで、ネットワーク遅延の影響を排除し、サーバ側の同時処理タイミングを揃える。HTTP/2 のマルチプレキシングを使うと簡単に実現可能(Burp Suite の Turbo Intruder で実装)。

4. 正しい防御 #defense

  1. DB トランザクション + 行ロックSELECT ... FOR UPDATE で「チェックと更新」を不可分に
  2. UNIQUE 制約 — 同じユーザによるクーポン使用を一意に。重複は DB レベルで弾く
  3. 楽観ロック(version カラム) — UPDATE 時に WHERE version = ? を付け、版が変わっていれば失敗
  4. 分散ロック — Redis SETNX、ZooKeeper 等で「同じリソースに対する処理は一度に1つ」を保証
  5. 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;