PR 本記事には広告(Amazonアソシエイト・もしもアフィリエイト・A8.net等)が含まれます。掲載情報の正確性には努めていますが、商品の詳細は必ずリンク先で最新情報をご確認ください。
CTF・セキュリティ学習

【レースコンディション編】タイミングの隙間を突く攻撃と守り方|CTF思考フレームワーク #12

かも次郎とアンペンが「レースコンディション」を解説するマスコットイラスト
安全に生きたい編集部

こんにちは、アンペンです!

前回は、サーバを内側から覗かせるSSRF/LFIを扱いました。

今回は、複数のリクエストが同じデータを取り合うときに起きるレースコンディションを見ていきます。「ボタン連打で残高以上のお金を引き出せる」ような、タイミングの隙間を突かれる攻撃です。

ボタンを連打するだけで攻撃になるって、本当?

サーバが『チェック→使う』の順で処理しているとき、チェックと更新の間に他のリクエストが入ると、本来できないはずのことが成立してしまうんだ。

『ボタンを連打するだけで攻撃になる』——そう聞くと、さすがに大げさでは?と思いますよね。でもレースコンディションは、まさにそれが現実に起きてしまう脆弱性です。しかも特別なツールも、難しい知識もいりません。ただ“同時に”操作するだけ。今日は、なぜそんな単純なことで残高以上のお金が引き出せてしまうのか、自販機のボタンにたとえながら見ていきましょう。

まず結論

レースコンディションは「チェックと更新の間にできる時間の隙間」を突く攻撃です。同時リクエストで残高超過の引き出し・クーポン多重利用・在庫超過予約などが起きるため、排他制御や原子的更新で隙間を作らない設計が必要です。

この記事で分かること

  • レースコンディションが成立する典型的な順序
  • TOCTOU(Time-of-Check Time-of-Use)という考え方
  • 排他制御・原子的更新・idempotencyキーの守り方
難易度:すこし慣れてから 所要時間:10分 体験:同時処理の挙動を観察 おすすめ:#11の後

📖 はじめてのWebセキュリティ #12|レースコンディション編
『同時に来たリクエスト』の扱い方を題材に、見えにくいタイミングの脆弱性を学びます。 シリーズ一覧を見る →

⚠️ 大事なお約束
この記事の確認は、CTF・公式ラボ・自分で作った検証環境だけで行ってください。実在するサービスに同時リクエストを浴びせる行為は、業務妨害や不正アクセス等に該当する可能性があります。

「チェック→使う」の間に隙間ができる

多くのサーバ処理は「条件を確認する → 条件を満たしたら更新する」という流れで動いています。たとえば、出金の場合は「残高が足りるか確認 → 足りたら引き落とす」です。

この2ステップの間に、同じ利用者の別リクエストが入り込めると、両方とも「残高あり」と判定された状態で2回引き落とされる、といったことが起きます。

ここで鍵になるのが“隙間”という言葉です。『確認する』と『引き落とす』は、人間の目には一瞬の出来事に見えます。でもコンピュータの世界では、その一瞬の中にさらに細かな時間が流れている。攻撃者は、その髪の毛一本ほどの隙間に、もう一つのリクエストをすべり込ませるんです。たった数ミリ秒の隙でも、狙ってそこを突けば再現できてしまう。だから「見えないほど短いから安全」は、まったく通用しません。

なぜ『順番』がそんなに大事なのか、銀行の例でもう少しゆっくり考えてみます。あなたの口座に1,000円あるとします。サーバは「残高は足りる?→足りるなら引く」の順で処理します。普通に1回ずつ操作するなら、何も問題ありません。ところがコンピュータは、リクエストが同時に来ると“同時並行”で処理しようとする。すると『2つの処理が、どちらも残高1,000円を見たまま』スタートしてしまう瞬間が生まれるんです。

図解:1リクエストと同時2リクエストの違い

残高チェックと出金を順番に処理した場合と、同時2リクエストが入った場合の比較タイムライン図
チェックと更新の間に隙間があると、両方とも条件を満たしてしまう。これがレースコンディション。

順番に1件ずつ処理する場合と、同時に2件が来た場合を並べると、隙間の有無が一目で分かります。

自動販売機で2つのボタンを同時押しすると、1枚のクーポンで2本の飲み物が出てしまうたとえ図
同時押し=1枚のクーポンで2本出てしまう。Webでも『チェック→消費』の隙間で同じことが起こる。
🥤 たとえるなら、自動販売機のボタン連打

1本だけ買えるクーポンを使って自販機で飲み物を買うとします。普通は「クーポンを確認→飲み物を出す→クーポンを消費する」と処理されますが、もし2つのボタンを同時に押せて、消費処理が後回しになっていたら、両方とも「クーポン有効」と判定されて2本出てしまうかもしれません。レースコンディションは、これがWeb上で起きる現象です。

ここで覚える用語:TOCTOU
Time-of-Check / Time-of-Use の略です。「確認した時点」と「実際に使う時点」の間に状態が変わってしまうことで起こる脆弱性パターン全般を指します。レースコンディションはこの代表例です。

TOCTOUは、ひと言でいえば『確認したときの世界と、実際に使うときの世界が、別物になっている』状態です。たとえるなら、冷蔵庫を開けて「プリンある!」と確認してから席に戻る数秒の間に、家族が食べてしまうようなもの。あなたの頭の中ではまだ“プリンはある”けれど、現実はもう変わっている。サーバの中でも、これとそっくりのすれ違いが起きているわけです。

代表的なレースコンディション3つ

レースコンディションの被害は、いろいろな顔で現れます。代表的なのは3つ。『残高以上のお金を引き出す』『1回限りのクーポンを何度も使う』『在庫1個の商品を複数人が買えてしまう』。どれも“同時に来たから、両方とも条件を満たしてしまった”という、同じ筋書きから生まれています。

よくある被害シナリオ

残高超過の出金・クーポン多重利用・在庫超過の購入の3シナリオを示したカード型インフォグラフィック
①残高超過 ②クーポン多重 ③在庫超過。守りは条件付き更新+ロック+ユニーク制約の3点。
  • 残高超過の出金:同時出金リクエストで、両方とも残高チェックを通過してしまう
  • クーポン・ポイント多重利用:1回限りのはずのクーポンが、同時利用で複数回適用される
  • 在庫超過の購入:残り1個の商品に複数注文が同時に届き、両方とも在庫ありと判定される

これらはバグではなく仕様上の隙間です。トランザクション・ロック・原子的更新を入れていない実装で広く発生します。

ここがレースコンディションの厄介なところ。これは『コードの書き間違い』ではないんです。一つひとつの処理は、仕様どおり正しく動いている。だから普通のテスト——1回ずつ順番に試すテスト——では、まず見つかりません。問題が顔を出すのは“同時に2つ来たとき”だけ。だからこそ、設計の段階で「同時に来たらどうなる?」を意識していないと、本番で初めて牙をむくことになります。

自分のラボで試すときも、ねらいは「荒らすこと」ではなく「同時に来たときのサーバの反応を見ること」です。同じリクエストを2つ同時に投げて、両方成功するか・片方だけ弾かれるかを観察するだけ。もちろん本物のサービスに連打リクエストを浴びせるのは、業務妨害にあたるので絶対にNGです。安全な砂場で“隙間の有無”を確かめましょう。

CTFでやってみよう:同時処理の挙動を観察する

やってみよう / 演習環境限定

自分の検証環境で、同じ操作を同時にリクエストしてみよう

目的は本物のサービスを荒らすことではなく、「サーバが同時リクエストにどう応答するか」を理解することです。

  1. CTFや自分の検証環境で、状態を変更する操作(クーポン適用や残高更新など)を選ぶ
  2. 開発者ツールやBurp Suiteなどで、同じリクエストを複数回同時に送る(自分のアカウント内)
  3. サーバの応答が両方とも成功するか、片方だけ成功するかを確認する
  4. サーバログがあれば、処理の順序とトランザクション開始/終了のタイミングを観察する
  5. SQLの実装を見られる場合は UPDATE ... WHERE balance >= ? のような原子的更新を使っているか確認する
他人のアカウント・本物のサービスでの同時リクエスト試行は絶対にやめてください。確認は自分のラボ・自分のテストアカウントの範囲だけです。

守りの主役になるのが『原子的(アトミック)な更新』という考え方です。言葉は難しそうですが、中身はとてもシンプル。

『原子的(アトミック)』って、また難しい言葉…どういう意味なの?

『これ以上分けられない、ひとかたまりの操作』という意味だよ。さっきの出金なら、「残高を確認してから引く」を2ステップに分けず、「残高が足りるなら、その場で引く」を“1回の操作”でやってしまう。途中に隙間がなければ、ほかのリクエストが割り込む余地もないよね。データベースに『残高が足りるときだけ引く』と一息で命じるイメージだよ。

守る側なら、「隙間を作らない」設計にしよう

レースコンディションの守りは、「チェックと更新を1つの操作にする」「同じリクエストが二重に処理されない仕組みを入れる」の2つが基本です。

守るための基本チェック
  • 条件付き更新を1つのSQL/操作で行う(例: UPDATE balance = balance - ? WHERE id = ? AND balance >= ?)
  • トランザクションと適切な分離レベルを利用し、SELECT FOR UPDATE などで行をロックする
  • クーポン・予約など状態が一意なものにはユニーク制約を入れて二重利用を物理的に防ぐ
  • 外部API呼び出しにはidempotency キーを導入し、二重送信を冪等に扱う
  • クライアント側でも、押下後はボタンを即時無効化する
  • 処理時間の長い操作にはジョブキューを挟み、ユーザリクエストとは別レーンで処理する

『チェック→更新』を1つにまとめる、っていう発想なんだね。

そう。隙間を作らない、もしくは『隙間に入っても矛盾しないルール』を持つ。これがレースコンディションへの基本姿勢だよ。

ここまでをひと言で言うと、レースコンディションの守りは『隙間を作らない』に尽きます。チェックと更新を一体にする、二重処理を物理的に禁じる(ユニーク制約)、同じ依頼は一度しか効かないようにする(冪等キー)。どれも“割り込む隙そのものをなくす”ための工夫です。

まとめ:タイミングの隙間は「設計」で塞ぐ

今回のポイント
  • レースコンディションはチェックと更新の隙間を突く攻撃
  • 残高超過・多重利用・在庫超過などの形で被害が出る
  • 守りは条件付き更新・ロック・ユニーク制約・冪等キーの組み合わせ
  • クライアント側のボタン制御は補助、本命はサーバ側の設計

今日の持ち帰りは『同時に来たらどうなる?を、いつも自分に問う』ことです。1回ずつなら正しく動く処理でも、同時に2つ来た瞬間に崩れることがある——この視点を持てるかどうかが、レースコンディションを防げるかの分かれ目になります。お金・在庫・クーポンなど“数が減るもの”を扱う処理では、特に強く意識してみてください。

次回は、仕様の隙間を突くビジネスロジックの脆弱性を扱います。「仕様上はできてしまうけど、本来は許してはいけない操作」をどう見抜くかを学びましょう。

次に読みたい記事

参考資料

記事URLをコピーしました