MENU

競合状態(Race Condition)の脆弱性とは?TOCTOU攻撃の仕組み・被害例・対策をわかりやすく解説

「ファイルの権限を確認した直後に、その中身がすり替えられている」——そんな一瞬のスキを突く攻撃手法があります。

競合状態(Race Condition)は、複数のプロセスやスレッドがほぼ同時に同一リソースへアクセスすることで、プログラムが想定外の動作をする脆弱性です。バッファオーバーフローやSQLインジェクションほど話題に上がらないものの、実際にはLinuxカーネルの特権昇格やWebアプリのポイント二重取りなど、深刻な被害が継続的に報告されています。

この記事では、競合状態の中でも特に注意すべき TOCTOU(Time-of-Check to Time-of-Use)攻撃 の仕組みを軸に、実際の被害事例・開発者と情シス担当者が今日から実践できる防御策まで体系的に解説します。

TOC

競合状態(Race Condition)とは?なぜ今重要なのか

競合状態とは、2つ以上の処理がほぼ同時に動作し、共有リソース(ファイル・データベース・メモリなど)への読み書きが意図しない順序で重なることで生じる脆弱性です。英語の「race condition」は、複数の処理がリソースを「取り合って競争する(race)」状態を指します。

プログラムは通常「確認 → 判断 → 実行」という順序で動作します。しかし、マルチスレッド環境や複数ユーザーが同時にアクセスするWebサーバーでは、「確認」と「実行」の間にほかの処理が割り込む余地があります。

たとえば、残高確認の直後に振込処理が走る前に、もう一つの振込リクエストが届いたとしましょう。両リクエストが「残高あり」と確認した状態で処理が進めば、実際には残高不足であっても2回の引き出しが通ってしまいます。

競合状態が発生しやすい主な環境は次のとおりです。

マルチスレッドアプリケーション: 同一プロセス内の複数スレッドが共有変数に同時アクセスする
Webアプリケーション: 複数ユーザーのリクエストが同じDBレコードに同時アクセスする
Linuxシステム: setuid/sgidプログラムがファイルパスを検証する際に別プロセスが介入する
クラウド・分散環境: 複数コンテナが同一のオブジェクトストレージを並行して操作する

競合状態の中でも、セキュリティ上特に危険なのが「TOCTOU攻撃」です。

TOCTOU攻撃の仕組み:「確認」と「使用」の間を突く

TOCTOU(Time-of-Check to Time-of-Use)攻撃とは、「確認(Check)した時点」と「実際に使用(Use)する時点」の間に生じる時間差に攻撃者が割り込む攻撃手法です。

典型的なシナリオは次のとおりです。

# 脆弱なコードの例(Python擬似コード) if os.access("/tmp/userfile", os.R_OK): # ① ファイルの読み取り権限を確認 # ↑ この一瞬の間に攻撃者がシンボリックリンクを差し替える with open("/tmp/userfile") as f: # ② 確認後にファイルを開く process(f.read())

攻撃者は①と②の間に、/tmp/userfile/etc/shadow(パスワードハッシュが格納された重要ファイル)などへのシンボリックリンクに置き換えます。プログラムはすでに権限確認を済ませているため、そのまま重要ファイルを読み込んでしまいます。

攻撃が成立するのは「チェックと使用の間に別の処理が割り込める」ことが前提です。具体的なステップは次のとおりです。

ステップ1(確認): プログラムがファイル・リソースの状態(権限・存在・内容)を確認する
ステップ2(攻撃者の介入): 確認直後の一瞬に、攻撃者がリソースの状態を変更する
ステップ3(使用): プログラムが「確認済み」の前提で処理を続行し、改ざん後のリソースを操作する

特に /tmp のような一般ユーザーが書き込めるディレクトリを経由するプログラムは、この攻撃の格好の標的になります。攻撃者はリクエストを自動化して大量に試みるため、一見「起きにくい」タイミングの問題でも現実的な脅威になります。

なお、競合状態の悪用は「不正アクセス禁止法」に抵触する可能性があります。自社システム以外での検証は必ず許可を取ったうえで実施し、詳細は法律の専門家にご確認ください。

実際の被害事例と影響の大きさ

競合状態・TOCTOU攻撃は理論上の話ではありません。過去に実際に確認された主な被害を見てみましょう。

1. Linuxカーネルの特権昇格:Dirty Pipe(CVE-2022-0847)

2022年に公開された「Dirty Pipe」は、Linuxカーネルのパイプ(プロセス間通信の仕組み)における競合状態を悪用した脆弱性です(NVD: CVE-2022-0847)。一般ユーザーが root 権限で保護されているファイルを上書きできるため、侵入後の権限昇格に悪用されました。Linuxカーネル 5.8以降の広範囲なバージョンに影響し、Amazon Linux・Android端末を含む多数の環境でパッチ適用が必要になりました。

2. Webアプリのポイント・残高の二重取り

ECサイトやポイントサービスでは、「残高確認→処理」の間に複数のリクエストを同時送信することで、1ポイントで複数回の商品交換を行う「二重取り」被害が継続的に報告されています。適切なトランザクション管理が実装されていない場合、この問題は容易に発生します。金融系APIでも類似の問題から不正送金につながった事例が国内外で確認されています。

3. /tmp を介した特権ファイルの上書き

setuidビット(特権で動作する設定)の付いたプログラムが /tmp に一時ファイルを作成する際、攻撃者が先にシンボリックリンクを配置しておく手法は1990年代から知られています。プログラムが意図せず /etc/passwd などを上書きし、一般ユーザーが root 権限を獲得するというパターンです。古典的な手法ながら、適切な対策が施されていないシステムでは今も有効に機能します。

具体的な防御手順

競合状態の防御は「確認と使用を分離しない」「共有リソースへのアクセスを排他制御する」の2方向が基本です。

1. アトミック操作で「確認と使用」を統合する

アトミック操作(原子的操作)とは、途中で割り込みが入らない不可分な処理のことです。「確認と使用」を一つの操作として扱うことで、TOCTOU攻撃の余地をなくします。

Pythonでは os.access() でチェックしてから open() するのではなく、直接 open() を試みて例外で処理する「EAFP(許可を求めるより許しを請え)」スタイルが推奨されています。

# NG: チェックしてから開く(TOCTOU脆弱性あり) if os.access("/tmp/userfile", os.R_OK): with open("/tmp/userfile") as f: data = f.read() # OK: 直接開いて例外で処理する(アトミック) try: with open("/tmp/userfile") as f: data = f.read() except PermissionError: handle_error()

データベース操作では「SELECT して確認してから UPDATE する」ではなく、条件付き UPDATE をアトミックに実行します。

-- NG: SELECTで確認してからUPDATE(競合状態が発生) SELECT balance FROM accounts WHERE id = 1; -- ↑ ここで別のトランザクションが割り込む可能性あり UPDATE accounts SET balance = balance - 1000 WHERE id = 1; -- OK: 条件付きUPDATEでアトミックに処理 UPDATE accounts SET balance = balance - 1000 WHERE id = 1 AND balance >= 1000; -- 影響行数が0なら処理をロールバック

2. ミューテックス・flockで排他制御する

共有リソースへのアクセスを一度に1つの処理に限定するのが排他制御(mutual exclusion)です。Linuxのシェルスクリプトでは flock コマンドでファイルロックを実装できます。

# flockで排他ロックを取得してから処理 ( flock -x 200 # ここに保護したい処理を記述 process_critical_section ) 200>/var/lock/myapp.lock

データベースのレイヤーでは、トランザクション分離レベルを適切に設定し、SELECT FOR UPDATE(行ロック)を活用することが重要です。

-- SELECT FOR UPDATE で行ロックを取得(競合トランザクションをブロック) START TRANSACTION; SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; UPDATE accounts SET balance = balance - 1000 WHERE id = 1; COMMIT;

3. /tmpを安全に使う:mktempでランダムファイル名を生成

/tmp を使う場合は、予測不能なファイル名を生成することでシンボリックリンク攻撃を防ぎます。

# NG: 固定ファイル名(予測・置き換えが容易) tmpfile="/tmp/myapp.tmp" echo "data" > "$tmpfile" # OK: mktempでランダムな一時ファイルを作成(O_EXCLフラグで既存ファイルをブロック) tmpfile=$(mktemp /tmp/myapp.XXXXXXXXXX) echo "data" > "$tmpfile" # 後片付け(trapで確実に削除) trap "rm -f '$tmpfile'" EXIT

Cプログラミングでは open()O_CREAT | O_EXCL フラグを組み合わせることで、シンボリックリンク経由の上書きを防げます。

4. setuidバイナリの棚卸しと最小権限化

そもそも特権で動くプログラムを減らすことが根本的な防御です。setuidビットを持つファイルを定期的に棚卸しし、不要なものは削除または権限を外します。

# setuidビット付きファイルをリストアップ find / -perm -4000 -type f 2>/dev/null # setgidビット付きファイルをリストアップ find / -perm -2000 -type f 2>/dev/null # 不要なsetuidビットを除去 chmod u-s /path/to/unnecessary-suid-binary

中小企業でも今日からできること

競合状態の対策は開発者だけの問題ではありません。情シス担当者が運用面で取り組めることもあります。

OSとカーネルの定期アップデート: Linuxカーネルの競合状態系CVEは毎年報告されます。dnf-automatic(RHEL系)や unattended-upgrades(Debian系)でカーネルアップデートを自動化するだけで、既知の競合状態脆弱性は大幅に防げます
setuidバイナリの月次棚卸し: 毎月1回 find / -perm -4000 を実行して、予期しないsetuidファイルが増えていないかを確認します。新規インストールしたパッケージが意図せずsetuidを付与するケースがあります
コードレビューへのチェックリスト追加: 「SELECTしてからUPDATE」「os.access()してからopen()」のパターンをコードレビューのNGリストに加えます。CIパイプラインにSAST(静的解析ツール)を組み込むと検出を自動化できます
DBトランザクション分離レベルの確認: 使用しているRDBMSのデフォルト分離レベルを確認しましょう。MySQLのInnoDBはデフォルトで REPEATABLE READ ですが、PostgreSQLは READ COMMITTED です。金融・在庫系の処理では SERIALIZABLE またはSELECT FOR UPDATE を検討します
レート制限でリクエスト大量送信を緩和: Webアプリケーションファイアウォール(WAF)やAPIゲートウェイで同一ユーザーからの高頻度リクエストを制限することで、競合状態を狙ったリクエスト攻撃を緩和できます

よくある誤解と注意点

【誤解1】「シングルスレッドなら安全」は間違い

Webサーバーはクライアントのリクエストを複数プロセス・スレッドで並行処理します。アプリケーションコード自体がシングルスレッドで書かれていても、複数ユーザーのリクエストが同時に処理されれば競合状態は発生します。Node.jsのようなシングルスレッドモデルでも、非同期処理の「awaits」をまたぐ間にほかの処理が割り込める点は同じです。

【誤解2】「チェックの回数を増やせば安全」は間違い

チェックをどれだけ繰り返しても、チェックと実行が別ステップである限りTOCTOUの問題は消えません。解決策は「アトミックな操作に統合する」ことです。チェック回数を増やすのはかえって処理を複雑にするだけで、安全性の向上にはなりません。

【誤解3】「タイミングが限られているから実害はない」は危険な認識

攻撃者はリクエストを自動化して短時間に何千回も試みます。確率が低い競合条件であっても、大量試行によって現実の攻撃に変わります。特にポイント・残高・在庫数に関わる処理は意図的に狙われる可能性が高いと考えてください。

【注意】デッドロックに注意して排他制御を設計する

排他制御やトランザクション分離レベルの変更は、デッドロック(複数の処理が互いのロック解放を待って永久に停止する状態)を引き起こすことがあります。本番環境への適用前に、高負荷シナリオを含めた十分なテストを実施してください。

よくある質問(FAQ)

Q. 競合状態とデータ競合(Data Race)は同じものですか?

似た概念ですが、厳密には異なります。データ競合(Data Race)は、同期機構なしで複数スレッドが同じメモリ領域に並行アクセスする特定の状態を指します。競合状態(Race Condition)はより広い概念で、データ競合を含む「実行順序に依存した想定外の動作」全般を指します。プログラミング言語によっては、データ競合があってもロック設計が誤っていて競合状態が発生する場合もあります。

Q. JavaScriptのasync/awaitでも競合状態は起きますか?

はい、発生します。Node.jsはシングルスレッドですが、await で処理を中断している間にほかの非同期処理が実行されます。「データを読んで await して書く」という処理の間に別のリクエストが同じデータを書き換えることがあります。データベースのトランザクションや楽観的ロック(バージョン番号の照合)で対応します。

Q. 既存のWebアプリで競合状態があるか調べる方法はありますか?

まずコードレビューで「チェックしてから使う」パターンを探します。次に、負荷テストツール(Apache JMeter・Locust など)で同一のポイント処理や在庫処理に同時リクエストを送り、数値に矛盾が生じないかを確認します。脆弱性スキャナーは競合状態の検出が苦手なため、自動化ツールだけに頼らず手動でのコード監査が重要です。

本記事のまとめ

項目 内容
競合状態とは 複数の処理が同じリソースへ同時アクセスし、想定外の動作が起きる脆弱性
TOCTOU攻撃とは 確認(Check)と使用(Use)の時間差に攻撃者が割り込む手法
主な被害 Linuxカーネルの特権昇格・Webアプリのポイント二重取り・重要ファイルの不正読み書き
対策① アトミック操作:確認と使用を不可分な1ステップに統合する
対策② 排他制御:flock・DBトランザクション・SELECT FOR UPDATEでロックする
対策③ /tmpの安全な使用:mktempでランダム名を使いシンボリックリンク攻撃を防ぐ
対策④ setuidの最小化:不要な特権バイナリを棚卸しして削除・権限を除去する
情シス向け対策 カーネルアップデート自動化・setuid月次棚卸し・コードレビューにアトミック操作チェックを追加

競合状態は「たまたま起きる偶発的な問題」ではなく、「条件が揃えば攻撃者が意図的に引き起こせる脆弱性」です。アプリケーション開発・サーバー運用の両面で「複数の処理が同時に動いたときどうなるか」を意識するだけで、リスクを大きく下げることができます。

Linuxのsystemdサービスやファイルパーミッション、setuidの詳細な管理方法については、姉妹サイトLinuxMaster.JPでも実践的に解説しています。

競合状態の対策、自社のコードやサーバーに適用できていますか?

TOCTOU攻撃やデータ競合は、適切な設計と実装で確実に防げます。
正しいセキュリティ知識を体系的に身につけたい方へ、メルマガで実践的なセキュリティ対策ノウハウをお届けしています。

Let's share this post !

Author of this article

TOC