何が起きるか
レースコンディションは、複数の処理が同じ状態を読み書きする順序によって、守るべき条件が崩れる問題です。説明用に一回限りのクーポンを考えます。
攻撃者が同じクーポンを使う注文をほぼ同時に二つ送ると、処理Aが未使用を読み、使用済みにする前に処理Bも未使用を読む場合があります。その後、両方が使用済みにして割引を確定すると、各要求は検査に成功していても、一回分のクーポンで二つの注文が割り引かれます。未使用の確認と使用済み更新を別々に行う隙間が原因です。
TOCTOUは、このうち確認した時点と使用する時点で状態や対象が変わる問題に注目する呼び方です。たとえば承認したブランチを攻撃者が別コミットへ更新し、ジョブが実行時のブランチ先頭を取れば、承認されていないコードを実行する場合があります。
正常ならどう動くか
一回限りのクーポンでは、「未使用の行だけを使用済みにする」という条件付き更新をDBへ一回で依頼します。更新件数が1件だった注文だけへ割引を付ければ、同時に来た二つの要求が同じクーポンで割引を受けることを防げます。未使用を読む処理と使用済みにする処理を、別々に実行しないためです。
デプロイの承認では、管理者が確認した不変のコミットIDを記録し、ジョブはそのIDで取得したコードを実行します。後から変わるブランチ名を使わないため、同じ名前を別の内容へ更新されても、未承認の内容へ置き換わりません。
不備と攻撃が成立する流れ
一回限りのクーポン利用を例にします。攻撃者が同じクーポンを使うHTTPリクエストをほぼ同時に二つ送ると、両方の処理がDBの使用済み更新前に「未使用」を読み取る場合があります。その後で両方が割引を確定すると、各処理は単独の検査に成功していても、一回限りの条件を破ります。
承認付きデプロイの例では、管理者がブランチの内容を確認した後、攻撃者がそのブランチを別のコミットへ更新します。ジョブが承認したコミットではなく、実行時のブランチ先頭を取得すると、確認されていないコードを承認済みとして実行します。ファイルの検査後にリンクなどを差し替える場合も、確認対象と使用対象が食い違います。
起こり得る被害と条件
二重利用の例では、一回分のクーポンで複数の注文へ割引が付き、攻撃者が本来払うべき代金を減らせます。送金の例では、複数の処理が同じ変更前の残高を見て送金を許可し、制限を超えて資金を移すおそれがあります。確定・取消処理の設計によって、最終的にどの状態が保存されるかは異なります。
データ不整合の例では、二つの更新が同じ古い設定を読み、一方の更新をもう一方が上書きします。攻撃者が狙った更新を後に保存させられると、管理者の変更が失われるおそれがあります。
未承認コード実行の例では、差し替えたコミットのコードがデプロイ用ジョブで動きます。説明用に、CIがデプロイ用トークンをジョブの環境変数へ設定する構成を考えます。差し替えたコードがその環境変数を読み、外部へHTTP送信できれば、攻撃者はトークンを取得できます。差し替えのタイミングに加え、ジョブへの秘密の提供と外部通信が条件です。
防ぎ方と、防げる理由
DBではトランザクション、適切なロック、条件付き更新、一意制約を用途に合わせて使います。CIでは承認を不変のコミットIDへ結び付けます。確認と使用の間に条件を崩せないようにするか、確認した対象と同じものを使うと保証するためです。
クーポンの説明例では、「使用済みがfalseの行だけをtrueに更新する」を一回のDB更新で行い、更新件数が1件の要求だけに割引を適用します。同時に二つ届いても片方だけが未使用の行を更新できるため、未使用確認と使用済み更新を別々に行う隙間をなくせます。
よくある誤解
直前にもう一度確認しても、その直後に差し替えられる隙間があれば解決しません。コミット作者の設定する時刻や、秒単位で等しい時刻も、内容が更新されていない証拠にはなりません。
実装の確認点
確認から使用・更新までの間にあるawait、外部API、別ジョブへの移行を調べます。通信完了などを待っている間にも別の要求が同じ状態を変更できるためです。複数のプロセスから同じDB行へ同時に処理が来ても制約を守れるか確認します。
CIでは承認したコミットIDであるSHAと、実際に取得・実行するSHAを比較します。ブランチ名や時刻だけで、承認した内容と実行する内容が同じと判断していないか確認します。
出典・確認範囲
- [1] Web Security Academy: レースコンディション・TOCTOUPortSwigger / 確認日 2026-10-29