CyberFix Note
セキュアコーディング

レースコンディション(競合状態)の仕組みと対策。チェックと使用の隙を原子性で塞ぐ

対象の目安: Webアプリ/バックエンドを実装する開発者 / 実務

ソウ攻撃・脆弱性リサーチ担当
・ 約14分で読めます
レースコンディション(競合状態)の仕組みと対策。チェックと使用の隙を原子性で塞ぐ

レースコンディション(競合状態)は、複数の処理が同じ資源に並行してアクセスするとき、実行の順序やタイミングによって結果が変わってしまう状態です。単発で動かす限り正しく見えるコードが、同時に何本も走った瞬間に破綻します。セキュリティの文脈でとくに問題になるのは、ある値を検査してから実際に使うまでの短い隙に、その値が別の処理によって変わってしまう場合です。この検査(check)と使用(use)のずれを突く典型が、TOCTOU(time-of-check to time-of-use)と呼ばれる欠陥です。

この記事は、WebアプリケーションとバックエンドAPIの開発者に向けて、なぜ並行アクセスが残高の二重引き出しやクーポンの多重適用、在庫超過といった不整合を生むのかを機構レベルで整理し、データベースの原子性やファイル操作の設計で塞ぐ方法をまとめます。攻撃の再現手順は扱わず、仕組みと防御実装に力点を置きます。

注意

本記事は防御実装の解説を目的とし、攻撃の再現手順は扱いません。参照先には検証手順を含むものがあります。挙動を確かめる場合は、自分が所有する環境か明示的な許可を得た環境に限り、不正アクセス禁止法などの適用法令や契約、利用規約を守ってください。

レースコンディションが成立する機構

問題の核心は一文で言えます。ある処理が「値を検査する」ことと「その値に基づいて状態を変える」ことを別々のステップに分けて行い、その二つが原子的でないため、間に別の処理が割り込んで状態を変えてしまうのです。

MITREのCWE-362は、この欠陥を、共有資源に一時的な排他アクセスを必要とする処理に、並行して動く別の処理がその資源を変更できる時間の窓が存在する状態と定義しています。正しく動くには、対象の資源へ排他的にアクセスできること(排他性)と、その一連の操作が途中で割り込まれないこと(原子性)の二つが要りますが、レースコンディションではこのどちらかが崩れています。

CWE-367は、その中でもとくに多い形として、資源の状態を検査してから使用するまでの間に状態が変わり、検査結果が無効になる欠陥をTOCTOUとして定義しています。攻撃者が検査と使用の間に資源の状態へ介入できるとき、検査は「その時点では正しかった」だけの意味しか持たなくなり、実際に使われるのは変化した後の資源になります。

MITREのCWE-362は、並行実行される処理が共有資源への一時的な排他アクセスを必要とするにもかかわらず、別の処理がその資源を変更できる時間の窓が存在する欠陥と定義しています。排他性と原子性という二つの性質が守られないことが原因で、割り込む処理は内部の信頼できるコードのこともあれば、外部の信頼できない発信元のこともあると説明しています。

Webアプリで起きる並行リクエストの不整合

Webアプリケーションでは、同じ利用者やセッションからのリクエストがサーバーで並行に処理されるときに、この隙が顔を出します。PortSwiggerのWeb Security Academyは、この種の欠陥をビジネスロジックの不備と密接に関連する脆弱性として整理し、アプリケーションが十分な保護なしにリクエストを並行処理すると、複数の処理が同じデータへ同時に触れて衝突(コリジョン)を起こすと説明しています。

分かりやすい例が、残高からの引き出しです。処理を「1. 残高が足りるか検査する」「2. 商品を渡す、または送金する」「3. 残高を減らす」の三段に分けて実装したとします。単発なら問題ありませんが、同じ利用者が残高ぎりぎりのリクエストをほぼ同時に二本送ると、両方が段階1を残高が減る前に通過し、二本とも段階2へ進んでしまいます。結果として残高以上の引き出しが成立します。同じ構図が、単回限りのギフトカードやクーポンの多重適用、招待コードの再利用、在庫数や定員の超過にもそのまま当てはまります。検査と更新の間に開いた窓へ、二本目のリクエストが滑り込む形です。

PortSwiggerのWeb Security Academyは、レースコンディションをリクエストの並行処理で生じる脆弱性と位置づけ、検査と変更と記録が別々のステップに分かれていると、最後の更新が完了する前の窓に重複リクエストが入り込むと説明しています。単回限りのギフトカードの複数回利用、割引コードの多重適用、残高を超える引き出し、レート制限の回避といった限度超過(limit overrun)を代表例として挙げています。

認可の判定も、状態の変化と組み合わさると同じ問題を抱えます。権限やアクセス対象の所有者を検査してから操作を実行するまでの間に対象が変わると、検査を通った操作が別の資源に効いてしまいます。認可の設計そのものの考え方は、次の記事で整理しています。

あわせて読みたい

IDORとアクセス制御の不備、識別子を差し替えられても守れる認可の作り方

ファイルと権限操作のTOCTOU

同じ機構は、Webの外側、ファイルや権限を扱うシステムプログラムでも起きます。CWE-367が挙げる古典的な例が、シンボリックリンクを使った差し替えです。特権を持つプログラムが、あるファイル名について「アクセスしてよいか」を検査し、その後で同じ名前を開いて読み書きするとします。検査と使用が別々のシステムコールに分かれていると、その隙に攻撃者がそのファイル名を機微なファイルへのシンボリックリンクに差し替えられます。プログラムは検査を通った安全なファイルのつもりで、実際には差し替え後のリンク先を操作してしまい、権限昇格やファイル改ざんにつながります。

共有された一時ファイルも同じ弱点を抱えます。予測できる名前で一時ファイルを作る前に「存在しないこと」を確認してから作成する実装では、確認と作成の間に攻撃者が同名のファイルやリンクを用意でき、意図しない書き込み先へ導かれます。ここでも、名前を検査してから名前で開くという二段構えが割り込みを許しています。

MITREのCWE-367は、資源の状態を検査してから使用するまでの間に状態が変わり、検査結果が無効になる欠陥をTOCTOU競合状態と定義し、CWE-362の子として位置づけています。緩和策としては、そもそも使用前の検査をしない、操作の途中で割り込まれないよう原子的に扱う、環境のロック機構で資源を保護してから検査する、使用後に資源を再確認する、といった方策を挙げています。検査と使用の時間差を小さくすることについては、問題を修正するものではなく攻撃の成立を難しくするだけだと明記しています。古典的な実例として access() で許可を確かめてから fopen() で開く実装を示し、両方がファイル名を扱うために隙が生まれると説明しています。ファイル名ではなくファイルディスクリプタに対して操作する設計は、この実例から導ける防御策です。

原子性で隙を塞ぐ対策

対策の原理は一つです。検査と、それに基づく状態変更を分割せず、途中で割り込めない一つの操作にまとめます。Webのデータ操作とファイル操作で、具体的な手立ては次のように整理できます。

領域危うい実装原子的にする手立て
残高や在庫の更新読み取ってからアプリ側で判定し書き戻す条件付きの原子的更新(UPDATE ... WHERE 残高 >= 金額)で、更新行数が0なら失敗とする
単回限りの適用使用済みか検査してから使用済みにする一意制約やユニークインデックスで重複挿入を DB に拒否させる
参照後の更新SELECT の値を前提に後続で更新行ロック(SELECT ... FOR UPDATE)や、バージョン列で衝突を検出する楽観ロック
重複リクエスト(通信再送やダブルクリック)同じ操作が何度も副作用を起こす冪等性キーで再送を吸収する。ただし業務制約は条件付き更新や一意制約、行ロックで守る
ファイル操作access() で検査してから open()副作用のない方法でファイルディスクリプタを取得し、fstat() で所有者と種別を検証してから同じディスクリプタに操作する
一時ファイル作成存在確認してから作成O_CREAT と O_EXCL の同時指定や mkstemp で、作成と排他を一操作にする

データベースを持つWebアプリでは、多くの場合データベースの原子性に寄せるのが確実です。アプリケーションのメモリ上で検査してから書き戻すと、その間はデータベースの外に状態があり、並行トランザクションから守れません。判定と更新を一つのSQL文にまとめる、あるいはトランザクションと適切な分離レベル、行ロックで囲むことで、検査と使用が一体になります。単回性は一意制約でデータベースに保証させるのが堅牢で、アプリ側の「使用済みか」の検査より取りこぼしがありません。

ファイル操作では、名前を検査してから同じ名前で開き直す二段構えをやめ、副作用のない方法でファイルディスクリプタを先に取得し、fstat() で所有者や種別を確かめてから同じディスクリプタに対して読み書きします。ただし通常の open() は途中のシンボリックリンクをたどり、O_TRUNC を付けると検証前にファイルが切り詰められ、O_NOFOLLOW はパス末尾のリンクしか防げません。Linux では、信頼済みのディレクトリFDを起点に openat() で開く、openat2() の RESOLVE_NO_SYMLINKS でリンク解決を禁じる、RESOLVE_BENEATH で指定ディレクトリの外へ出させない、といった手段でリンク差し替えの余地を狭められます。

  1. 1

    検査と副作用が分かれている箇所を洗い出す

    残高や在庫、ポイント、クーポン、招待コード、レート制限のように「検査してから状態を変える」処理を洗い出します。とくに同一利用者の並行リクエストで壊れうる箇所と、アプリのメモリ上で判定して書き戻している箇所に注目します。

  2. 2

    判定と更新を一つの原子的操作にまとめる

    可能な箇所は、条件付きの原子的更新(UPDATE ... WHERE)にして、更新できた行数で成否を判定します。参照した値を前提に後続で更新する処理は、行ロックやバージョン列による楽観ロックで、間に他の更新が入っていないことを保証します。

  3. 3

    単回性は一意制約でデータベースに守らせる

    クーポンの適用や一回限りの発行は、利用者と対象の組み合わせに一意制約やユニークインデックスを張り、二重挿入をデータベースに拒否させます。アプリ側の使用済みチェックだけに頼らないようにします。

  4. 4

    重複リクエストに冪等性キーを導入する

    決済や送金のように副作用が大きい操作には、クライアントが生成する冪等性キーを受け取り、同一キーの二回目以降は処理を再実行せず最初の結果を返します。キーは利用者や操作の単位でスコープを絞り、ペイロードの照合、処理中状態を原子的に確保する仕組み、結果の永続化、有効期限までを設計します。これは通信再送やダブルクリックを吸収する補助策で、残高やクーポン上限といった業務制約は前段の条件付き更新や一意制約、行ロックで守ります。

  5. 5

    ファイル操作は安全に取得したディスクリプタに対して行う

    ファイル名で検査してから名前で開き直す二段構えをやめ、副作用のない方法でファイルディスクリプタを取得し、fstat() で所有者と種別を確かめてから同じディスクリプタに操作します。Linux では信頼済みディレクトリFDを起点にした openat() や、openat2() のリンク解決制限も使えます。一時ファイルは O_CREAT と O_EXCL の同時指定や mkstemp で、作成と排他を一操作にします。あわせてプロセスの権限を最小に絞り、万一の割り込み時の影響を限定します。

複数プロセスやサーバーにまたがる処理では、データベースだけで守れない場合に分散ロックで排他区間を作る選択肢もあります。ただしロックは取得漏れや解放漏れ、保持時間の設計を誤ると別の不具合を招くため、まずはデータベースの原子性と一意制約で守れないかを先に検討します。入力の検証や認証と同じく、単一の対策に頼らず、原子的な設計を土台に据えたうえで最小権限や監視を重ねる姿勢が有効です。

外部入力を安全に扱う設計全般の考え方は、次の記事とあわせて読むと土台が固まります。

あわせて読みたい

入力バリデーションと出力エスケープの原則。入口で検証し、出口で文脈別にエスケープする

まとめ

レースコンディション(CWE-362)は、共有資源への排他的で割り込まれないアクセスが必要な処理に、別の処理が入り込める時間の窓が開いている欠陥です。その代表であるTOCTOU(CWE-367)は、資源を検査してから使用するまでの隙に状態が変わり、検査結果が無意味になります。Webでは同一利用者の並行リクエストが残高やクーポンの検査を同時に通過して二重引き出しや多重適用を招き、システムではファイル名の検査と使用の隙にシンボリックリンクが差し替えられます。

対策の要点は、検査とそれに基づく状態変更を分割せず原子的に行うことです。データベースでは条件付きの原子的更新や行ロック、単回性を守る一意制約で隙をなくし、ファイル操作では名前ではなく安全に取得したファイルディスクリプタに対して操作します。決済のように副作用の大きい操作では、冪等性キーで通信再送やダブルクリックを吸収します。危険なタイミングを都度潰そうとするのではなく、そもそも割り込む窓を残さない設計にすることが出発点です。

レースコンディション対策で確認したいポイント

  • 残高や在庫、クーポン、招待コード、レート制限など、検査してから状態を変える処理を洗い出したか
  • アプリのメモリ上で判定して書き戻すのではなく、条件付きの原子的更新(UPDATE ... WHERE)で成否を判定しているか
  • 参照した値を前提に更新する処理を、行ロックやバージョン列の楽観ロックで守っているか
  • 単回限りの適用を、アプリのチェックだけでなく一意制約でデータベースに守らせているか
  • 決済や送金などの副作用が大きい操作で、冪等性キーで通信再送やダブルクリックを吸収しつつ、業務制約は原子的更新や一意制約で守っているか
  • ファイル操作を名前ではなく安全に取得したファイルディスクリプタに対して行い、一時ファイルを O_EXCL や mkstemp で安全に作っているか
  • 分散ロックに頼る前に、データベースの原子性と一意制約で守れないかを先に検討したか

認可の判定と状態変更が絡む不整合は、アクセス制御の設計そのものを見直すと根から減らせます。関連する考え方は次の記事で扱っています。

あわせて読みたい

認証と認可の違い。Authentication と Authorization を基礎から整理する

出典・参考

この記事をシェア

関連する記事

セキュアコーディング

NoSQLインジェクションの仕組みと対策。クエリ演算子の注入で認証回避やデータ抽出に至る欠陥を塞ぐ

NoSQLインジェクション(CWE-943相当)を発生原理から対策まで整理します。信頼できない入力がクエリのオブジェクト構造や演算子として解釈され、認証回避やデータ抽出に至る仕組みと、型検証や演算子の拒否、安全なクエリAPI、$whereの不使用、最小権限による根本対策を開発者向けに解説します。

セキュアコーディング

プロトタイプ汚染(Prototype Pollution)の仕組みと対策。継承を断ちObject.prototypeを守る

プロトタイプ汚染(CWE-1321)を発生原理から対策まで開発者目線で整理します。信頼できない入力を安全でない再帰マージやプロパティ設定で扱うと、__proto__やconstructorを通じて共有されるObject.prototypeが汚染され、ほぼ全てのオブジェクトに影響します。Object.create(null)やMapの利用、危険キーの拒否で塞ぐ方法を解説します。