【入門編】 Race Condition (競合状態) を利用したトランザクション操作 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

こんにちは!セキュリティの世界へようこそ。
新人のIT担当者や、これからWeb開発のセキュリティをしっかり学んでいきたいと考えている皆さん、日々の開発やお疲れ様です。

セキュリティの勉強をしていると、「SQLインジェクション」や「クロスサイトスクリプティング(XSS)」といった有名な言葉はよく耳にするようになりますよね。でも、今回お話しする「Race Condition(競合状態:きょうごうじょうたい)」という脆弱性は、少し毛色が違います。コードの書き方には一見間違いがなさそうなのに、「同時にたくさんの人がアクセスしてきた瞬間」にだけ、システムがパニックを起こして不正を許してしまうという、ちょっと厄介なバグなんです。

今回は、この「競合状態」がどのように悪用されるのか、そして私たちが作るシステムをどうやって守ればいいのかを、身近な例えを交えながら一歩ずつ優しく紐解いていきましょう!

—

1. 身近な例えで理解する「競合状態(Race Condition)」

まずは難しいコンピュータの話をちょっと置いておいて、私たちの身近な生活にある「自動販売機」を想像してみてください。

今、その自動販売機には「大人気の限定ジュース」が残り「1本」だけ入っています。

ここに、A君とB君という2人のお客さんがやってきました。2人とも「どうしてもそのジュースが飲みたい!」と思い、全く同じタイミングで100円玉を入れ、ボタンを「ポチッ」と同時に押しました。

ここで、自動販売機の頭の中(システム)で何が起きているでしょうか?

1. 在庫の確認(チェック): 機械「おっ、いま在庫は1本あるな!」(A君の処理)/ 機械「おっ、いま在庫は1本あるな!」(B君の処理)
2. お金の処理と払い出し: 機械「じゃあジュースを出すね!」(A君に1本、B君にも1本……あれ?)

そうなんです。残り1本しかないのに、2人が同時に「在庫があるか確認する作業」をしてしまったため、機械が「まだある!」と勘違いして、2人に向けてジュースを払い出してしまいました。結果として、お店側は1本しか在庫がないのに2本奪われてしまった(大赤字!)ことになります。これが、まさに「競合状態」の仕組みです。

サイバー攻撃者は、この「同時に処理が走った瞬間のすき(タイミングのズレ)」を、専用のプログラムを使って秒間に何百回、何千回という猛スピードで狙い撃ちします。これが、在庫の不正買い占めや、銀行口座の残高ロンダリング(同じ残高で何度も引き出す)といった恐ろしいトランザクション操作に繋がるわけです。

—

2. 脆弱なプログラムの裏側を覗いてみよう

では、実際のWebアプリケーションでは、これがどのように起きてしまうのかを見てみましょう。
よくある「オンラインショップの在庫を減らす処理」をPHPで書いてみました。

<?php
// 【危険な実装例】データベースの仕組みを無視したナイーブなコード
// ※実際にはDB接続やセッション処理が前後にあります

$productId = 100; // 対象の商品ID

// 1. 現在の在庫数をデータベースから読み取る
$sql = "SELECT stock FROM products WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $productId]);
$product = $stmt->fetch();

$currentStock = $product['stock'];

// 2. 在庫が1つ以上あるかチェックする
if ($currentStock > 0) {
    // 処理の重さを再現するために、少しだけ時間を空ける(実際のシステムでも重い処理は存在します)
    usleep(100000); // 0.1秒待つ

    // 3. 在庫を「今の在庫 - 1」に更新する
    $newStock = $currentStock - 1;
    $updateSql = "UPDATE products SET stock = :stock WHERE id = :id";
    $updateStmt = $pdo->prepare($updateSql);
    $updateStmt->execute([':stock' => $newStock, ':id' => $productId]);

    echo "購入成功!残りの在庫は " . $newStock . " 個です。";
} else {
    echo "売り切れです!";
}
?>

このコードのどこが問題かわかりますでしょうか?
ステップ1(在庫の読み取り)と、ステップ3(在庫の更新)の間に、ほんのわずかな「隙間(タイムラグ)」がありますよね。

もし、ユーザーAとユーザーBが、このプログラムに全く同じマイクロ秒(1秒の100万分の1)でアクセスしてきたらどうなるでしょう?
2人とも「ステップ1」で $currentStock = 1 を読み取ってしまいます。その後、2人とも 1 - 1 = 0 という計算をし、データベースに「在庫は0個です」と書き込みます。
本来なら2人が買ったら在庫はマイナス(破綻)になるはずが、1個買ったことになってしまい、注文データだけが2つすり抜けて成立してしまうのです。

—

3. どうやって防ぐの?データベースの「ロック」という最強の盾

「うわぁ、怖いな……じゃあどうやってこの同時アクセスをさばけばいいの?」と不安になりますよね。安心してください。データベースには、こうした混乱を防ぐための「排他制御(はいたせいぎょ)」という素晴らしい仕組みが用意されています。

防犯の例えで言うと、「トイレや試着室のドアに鍵をかける」のと同じです。
誰かが中に入って作業している間は、他の人は外で待たなければいけませんよね。データベースの世界でも、この「鍵」をかけることで安全性を保ちます。

ここでは、実務でよく使われる2つのアプローチを優しく見ていきましょう。

① 悲観的ロック (Pessimistic Locking)

「どうせ他の人と取り合いになるに違いない!」と悲観的に(用心深く)考え、最初にデータを触った瞬間に、他の人がそのデータを触れないように鍵(ロック)をかけてしまう方法です。

SQLを書くときは、FOR UPDATE という魔法の言葉を使います。

-- 【安全な実装例:悲観的ロック】
-- トランザクションを開始する
START TRANSACTION;

-- 1. データを取得すると同時に、他の人が書き換えられないように「鍵(ロック)」をかける
SELECT stock FROM products WHERE id = 100 FOR UPDATE;

-- (この間、他の人はこの商品の行を読み書きしたくても、ロックが外れるまで待たされます!)

-- 2. 在庫を減らす処理
UPDATE products SET stock = stock - 1 WHERE id = 100;

-- 3. 処理を確定させる
COMMIT;

これなら、A君が処理をしている間、B君のアクセスはステップ1の SELECT ... FOR UPDATE の場所で強制的に待たされます。A君の処理が終わって COMMIT(鍵を開ける)されるまでB君は進めないので、在庫の計算が狂うことは絶対にありません!

② 楽観的ロック (Optimistic Locking)

「まあ、基本的には同時にアクセスされることは滅多にない(楽観的)だろう」と考え、鍵はかけずに作業を進め、最後に保存するタイミングで「他の人に先に書き換えられていなかったか?」を確認する方法です。

これには、テーブルに「バージョン番号(versionカラムなど)」を持たせるのが一般的です。

-- 【安全な実装例:楽観的ロック】

-- 1. データを取得する(現在のバージョンが「5」だったとする)
-- SELECT stock, version FROM products WHERE id = 100;

-- 2. データを更新する時に、バージョンが「5」のままであることを条件にする
UPDATE products 
SET stock = stock - 1, version = version + 1 
WHERE id = 100 AND version = 5;

もし、この更新を実行する瞬間に、別の誰かがすでにこのデータを更新してバージョンが「6」になっていたらどうなるでしょうか?
WHERE version = 5 の条件に一致しなくなるため、「更新件数:0件(失敗)」という結果が返ってきます。システム側で「おっと、他の人に先を越されたな」と気づくことができるため、エラー画面を出したり、もう一度最初から処理をやり直させたり(リトライ)することが可能になります。

—

4. 実務の現場で意識すべきポイントとまとめ

いかがでしょうか?「Race Condition(競合状態)」の正体と、それを防ぐための「ロック」の仕組みが、少しイメージできたのではないでしょうか。

実務の現場で開発を行う際は、以下のポイントを心に留めておいてください。

1. お金、在庫、ポイント、チケット枚数など、数が減ると困る・あるいは不正に増えると致命的な「重要リソース」を扱う処理では、必ず排他制御(悲観的ロックや楽観的ロック)が実装されているかコードレビューで確認する。
2. アプリケーション層(PHPやPythonなど)の変数チェックだけで安心せず、データベースのトランザクションとロック機能の仕組みを正しく理解して設計する。
3. ペネトレーションテストや脆弱性診断を行う際は、Burp Suite等のツールを使って複数のリクエストを同時に送信(Send to Intruder / Parallel requests)し、意図した通りの排他制御ができているかをテストする。

セキュリティの対策は、一度覚えてしまえば次の開発からすぐに武器になります。「なんだか難しそう」と身構えず、身近な例えを思い出しながら、一歩ずつ安全なコードを書けるエンジニアを目指していきましょう!

それでは、また次のセキュリティ解説でお会いしましょう。お疲れ様でした!

コメント

タイトルとURLをコピーしました