レースコンディション|タイミングのズレを突く同時リクエスト攻撃
第4話まではサーバーが返す「応答の違い」を手がかりにする攻撃を見てきました。第5話は少し視点を変えます。サーバーの処理そのものに「チェックしてから書き込もまでの、ほんの一瞬の雕」があると、複数のリクエストを同時に送るだけで、本来あり得ない結果を引き出せてしまいます。この「レースコンディション」という脆弱性クラスを、残高システムを舞台に体験します。
📋 目次
⏱️ レースコンディションとは何か|チェックと書き込みの間に生まれる雕
個別には正しい処理が、同時に来ると壊れる
最後の1席に2人が同時に予約ボタンを押したら
イベント予約システムで残り1席を2人が同時に見ているとき、どちらも「まだ空いている」と確認(チェック)した直後に予約(書き込み)を確定させると、2人とも「予約できました」と表示されてしまうことがあります。「確認した時点」と「実際に使う時点」の間に、他の人が同じ情報を利用していたためです。
このパターンは「TOCTOU(Time-Of-Check to Time-Of-Use)」と呼ばれ、チェックした時間と実際にその結果を使う時間の間に生まれるすべての不具合の縁名です。Webアプリでは、「クーポンが未使用か確認→使用済みに備付け」「残高が十分か確認→引き出しを実行」「在庫があるか確認→注文を確定」など、「確認してから実行する」最も基本的な処理パターンの中に潜んでいます。
この話の前提知識
第4話(パディングオラクル攻撃)では「サーバーの実装の小さな雕」を手がかりにしました。今回は「処理の順序」そのものが雕になります。
- 上級編 第4話「パディングオラクル攻撃|「復号成功/失敗」だけで暗号文を解く」
TOCTOUは新しい概念ではなく、CWE(Common Weakness Enumeration)でも𰃌WE-367」として古くから分類されている、ソフトウェア工学の基本的な欠陥パターンの一つです。ファイルシステムの操作やOSのプロセス管理など、複数の処理が同時に走る環境では何十年も前から知られています。Webアプリケーションが普及し、1つのサーバーが同時に何千ものリクエストを処理するようになったことで、この古典的な欠陥が新しい形で表面化しています。
🧮 TOCTOUの正体|チェック・待機・書き込みの3段階
待機時間はDB問い合わせや外部API呼び出しなど、どこにでも潜んでいる
ほとんどのWebアプリの処理は、極簡化すると次の3段階で成り立っています。①チェック(現在の状態を読む)②待機(DB問い合わせや外部API呼び出しなどのI/O待ち)③書き込み(状態を更新する)。②の待機は何もしていないわけではなく、データベースやキャッシュシステムとの通信、ログ記録など、現実には必ず何厘秒かの時間がかかります。
| 実例 | チェック | 書き込み | 悪用されると |
|---|---|---|---|
| クーポン利用 | 未使用か | 使用済みに備付け | 1枚を何度も使い回せる |
| ポイント引き出し | 残高が十分か | 残高を減算 | 持ち残高以上に引き出せる |
| 在庫販売 | 在庫があるか | 注文を確定 | 在庫1個を複数人に二重販売 |
| CTFのfirst-blood | 未解決か | 最初の正解として記録 | 同時提出で複数人が一位を主張 |
個別には正しいコードが、同時実行で壊れる
「残高が十分か確認してから減算する」というコードは、1件ずつ順番に実行される限り正しく動きます。しかし同じ処理が同時に複数呼ばれると、全員が「残高が十分」と確認した直後の状態で減算を進めてしまい、全員が成功してしまうことがあります。コードそれ自体は1文字も変わっていないのに、呼び方(同時性)だけで結果が変わるのがこの脆弱性の根深さです。第4話のパディングオラクル攻撃もサーバーの応答という手がかりを使いましたが、今回は応答の中身ではなく「処理のタイミング」そのものが手がかりになる点が大きな違いです。
🔍 なぜ気づきにくいのか|「いつも通り」動いているように見える
通常のテストではほとんど再現しない
レースコンディションの厳しい点は、1件ずつ順番にオク・テストする限り完全に正常に動く点です。QAエンジニアが手動で1回ずつテストしても、コードレビゥーで処理の流れを1本ずつ読んでも、どちらも問題に気づきません。問題が表面化するのは、本番環境で大量のユーザーが同時にアクセスし、たまたま雕のタイミングで複数のリクエストが重なった時だけです。
バグバウンティでも頻出する定番クラス
決済・クーポン・ガチャなど、「何かを1回だけ使える」機能を持つサービスでは、レースコンディションが今でも頻出する定番の報告クラスです。「クーポン券を何百回も使えた」「残高ゼロのギフトカードで买い物ができた」といった報告は、バグバウンティプログラムで毎年報告されています。
注文機能も同じ構造的な雕を持ちやすい処理の代表例です。「在庫数を確認してから注文を確定させる」という流れは、残高やクーポンの例と完全に同じ構造をしています。在庫1個に対して注文が1件だけ来るという通常時の動作確認だけでは、この問題に気づくことはできません。
💰 実践チャレンジ:残高システムの雕を突いて引き出せ
同時に送るだけで、持ち残高以上を引き出せる
架空の「やさいポイント」システムには、1回の引き出しで300ポイントを減算する機能があります。初期残高�ポイントです。内部の処理は「残高を確認(チェック)→少し待機(処理遲延を再現)→残高を減算(書き込み)」という手順で動いています。
💳 現在の残高:1000 pt
🚨 不正検知:残高がマイナスになりました
不正利用の可能性があるため、このアカウントを一時凍結し、インシデントID: としてセキュリティチームに通報しました。
ステー𰮁をクリアすると、ステー𰮂が解放されます。両方クリアした時点でスコアが確定します。
📊 ステージ進捗: 0/2|挑戦回数: 0回
上の「5回同時に引き出す」ボタンを押して、残高がマイナスになる様子を確認し、最終的な残高を使ってRACE-NEG〇〇〇形式で入力してください(例:RACE-NEG100)。
「1回だけ引き出す」を何回押しても、残高が300未満になった時点で正しく失敗します。「5回同時」の方を試してください。
1000ptから300pt×5回全部成功すると、残高は-500ptになります。
不正検知パネルに表示された「インシデントID」をそのまま下に入力してください。
上の「不正検知:残高がマイナスになりました」パネル内の赤い枠の中に、インシデントID:として表示されています。
🛡️ 防御側の視点|レースコンディションをどう防ぐか
「チェック」と「書き込み」を一体化する
- アトミック操作:DBの
UPDATE accounts SET balance=balance-? WHERE id=? AND balance≥?のように、チェックと書き込みを1つの不可分な命令にする - ロック(悲観的排他制御):
SELECT ... FOR UPDATEで行をロックし、他のトランザクションが同じ行を同時に書き書き込めないようにする - 冪等性キー:一意なリクエストID(Idempotency Key)を発行し、同じIDのリクエストは2回上処理しない(決済APIの標準対策)
- トランザクション分離レベル:SERIALIZABLE等の高い分離レベルを使い、並行実行時に矛盾する更新をDB自体に拒否させる
「読んでから書く」ではなく「書きながら確認する」
今回の脆弱な実装は「残高を読む→判断する→減らす」という3つの操作が分離していることが原因でした。安全な実装は「減らすと同時に十分な残高があるかを確認する」という単一の不可分操作にすることで、雕そのものをなくします。
データベースが行単位の更新を内部で必ず順番に実行するという仕組みを利用すれば、アプリケーション側で明示的にロックを書かなくても安全になります。重要なのは「チェックと書き込みを別の文として書かない」という原則であり、そのための手段はDBや言語エコシステムによっていくつも用意されています。
📝 まとめ+FAQ+次回予告
雕を突いて、残高をマイナスにした
第5話では、チェックと書き込みの間の雕を突くレースコンディションを体験しました。次回はクラウドIAM権限昇格という、個別には無害な権限の積み重ねが特権昇格につながる構造を扱います。
・レースコンディション(TOCTOU)は「チェック」と「使用」の間の時間差を悪用する
・同時リクエストだけで、残高以上の引き出しやクーポンの二重使用ができてしまう
・個別のコードは正しくても、同時実行されると壊れるのが本脆弱性の難しさ
・最強の防御はチェックと書き込みを1つの原子操作(原子性)にすること
Q. 実際のサービスで、レースコンディションは今でも発見されていますか?
はい。決済・クーポン・フラッシュセールの在庫管理など、「一度だけ」を保証したい機能では今でも頻出するバグバウンティの報告対象です。
Q. 同時リクエストを送るだけで、本当に不正と認定されてしまうのですか?
実装によります。本話のラボは教材用に最も単純な実装を再現していますが、実務ではプログラトーー(ためし比較更新)やガードバースト検知と組み合わせることで、不正な呼び出しだけを高精度に検知します。
Q. 今回のシミュレートは実際のネットワーク通信で同時性を再現していますか?
いいえ、PromiseとsetTimeoutだけで再現しています。実際のネットワーク通信やWorkerを使うと環境によってタイミングが不安定になり再現性が下がるため、JavaScriptのイベントループの挙動を利用して、どの環境でも同じ結果になるよう設計しています。
Q. 残高やアカウントの情報はどこかに送信されますか?
送信されません。すべてブラウザのlocalStorage(あなたの端末内)だけに保存され、外部のサーバーには一切送信されません。
クラウドIAM権限昇格|小さな許可設定の積み重ねがもたらす侵害
個別には無害に見える権限の組み合わせが特権昇格を生む構造を体験します。
📚 参考情報
- OWASP Top 10(A04:2021 Insecure Design)
- CWE-367(Time-of-check Time-of-use Race Condition)


コメント