こんにちは!セキュリティの世界へようこそ。
日々の開発やインフラの管理、本当にお疲れ様です。
システムを作るとき、「ちゃんとしたパスワードを使おう」「通信は暗号化(HTTPS)しよう」といった基本的なセキュリティ対策は、だいたい教科書通りに実装できるようになりますよね。でも、ふとこんな不安を感じたことはありませんか?
「セキュリティのチェックは全部クリアしたはずなのに、なぜかオンラインショップで商品をタダ同然で買われてしまった……」
「会員登録の仕組みがおかしくなって、ポイントが無限に増やされてしまった……」
こういったトラブルの原因になるのが、今回お話しする「ビジネスロジックの脆弱性(Business Logic Vulnerabilities)」です。
難しそうな名前がついていますが、要するに「プログラムのバグではなく、人間の考える『ルールの穴』や『手順の勘違い』を突いたズルっこ」のこと。
今回は、身近な例えを交えながら、この厄介な脆弱性の正体と対策を優しく紐解いていきましょう!
—
1. 家の鍵に例える「ビジネスロジックの脆弱性」
セキュリティの基本として、私たちはよく「家の鍵」に例え話をします。
- 通常の脆弱性(SQLインジェクションやXSSなど):
頑丈なドアに「ドリルで穴を開けて鍵をこじ開ける」ような、技術的な力技の侵入方法です。
- ビジネスロジックの脆弱性:
ドアも鍵もピカピカの最新式で、絶対に壊せないとします。しかし、「郵便受けのフラップから手を突っ込んで、内側の鍵をガチャリと回して開けてしまった」というような状態です。
鍵は壊していません。泥棒は「開いている隙間」を使っただけ。システム側からすれば「正規の手順で鍵が開けられた」と認識してしまうため、自動化されたセキュリティツール(いわゆるスキャナー)では、なかなか見つけることができません。
—
2. 実際の攻撃メカニズムを見てみよう
では、Webサイトでは具体的にどんなことが起きるのでしょうか?
よくある「オンラインショップの価格操作」を例に、攻撃のメカニズムを覗いてみましょう。
カートから決済までの「手順」を飛ばすワープ技
通常、私たちがネットショッピングをするときは、次のような順番(ワークフロー)を辿りますよね。
1. 商品をカートに入れる
2. 住所や支払い方法を入力する
3. 最終確認画面で合計金額を確認する
4. 決済ボタンを押して購入完了する
開発者としての普通の思い込みは、「人間なら必ずステップ1から順番に画面をクリックして進むだろう」というものです。
しかし、攻撃者は画面のボタンをポチポチ押したりしません。ブラウザの機能や、リクエストを自由にいじれるツール(Burp Suiteなど)を使って、いきなりステップ3を飛ばしてステップ4(決済完了API)にアクセスしてしまいます。
もし、サーバー側で「このユーザーはちゃんと合計金額を確認したか?」というチェック(状態管理)をサボっているとどうなるでしょうか?
なんと、初期値の「0円」や、自分で勝手に書き換えた「1円」というパラメータで決済が通ってしまうのです。これがビジネスロジックの裏をかく瞬間です。
—
3. 具体的なコード例で学ぶ「危ない実装」と「正しい実装」
それでは、もう少し具体的にコードを見てみましょう。
ECサイトで商品をカートに入れて、数量や価格を送信する処理をイメージしてください。
【危ない実装例(PHP)】
以下のコードは、前端(ブラウザ)から送られてきた価格(price)と数量(quantity)をそのまま信じて合計金額を計算し、決済処理を進めてしまう非常に危険な例です。
<?php
// 【危険】ブラウザから送信されたデータを無条件で信頼している例
$productId = $_POST['product_id'];
$quantity = $_POST['quantity'];
$price = $_POST['price']; // ★危険:ユーザーが価格を自由にいじれてしまう!
// 単純に掛け算して合計金額を算出
$totalAmount = $price * $quantity;
// 決済処理を実行
processPayment($totalAmount);
?>
ブラウザの「要素の検証(デベロッパーツール)」などを開けば、隠しフィールド(<input type="hidden" name="price" value="10000">)の数値を「1」に書き換えるのは、ほんの数秒でできてしまいますよね。
【正しい実装例(修正版)】
価格や商品の実データは、絶対にユーザー(ブラウザ)側に持たせないのが鉄則です。サーバー側(データベース)に保存されている正しい価格を引っ張ってきて計算しましょう。
<?php
// 【安全】サーバー側(データベース等)のデータを信頼する例
$productId = $_POST['product_id'];
$quantity = $_POST['quantity'];
// 1. データベースから商品の「本当の価格」を取得する
$productFromDB = getProductFromDatabase($productId);
$safePrice = $productFromDB['price']; // データベースの価格を使用
// 2. サーバー側で安全に合計金額を計算
$totalAmount = $safePrice * $quantity;
// 3. 数量がマイナスになっていないかなどの「ワークフローの常識」もチェック
if ($quantity <= 0) {
throw new Exception("不正な数量です。");
}
// 決済処理を実行
processPayment($totalAmount);
?>
このように、「信頼できるのは自分(サーバー)のデータだけ。ユーザーから送られてくるデータはすべて疑う」というゼロトラストの精神が、ビジネスロジックを守る最大の盾になります。
—
4. 防御とインシデントハンドリングの現場の知見
実務の現場では、こうしたロジックの隙を見つけるためにペネトレーションテスト(模擬ハッキング)が行われます。もし、あなたが新しくサービスをリリースする立場や、インフラ・アプリケーションを守る立場になったら、次のポイントを意識してみてください。
A. ワークフローの「状態(ステータス)」を厳密に管理する
「ステップAが終わっていなければ、ステップBに進めない」というガードを、セッションやトークンを使って必ずサーバー側で管理しましょう。「画面を表示したか」ではなく、「サーバー側で処理が完了したか」が判断基準です。
B. 境界値やマイナスの世界を疑う
数量に「-1」を入れたらどうなるか? クーポンコードを1回の会計で100回適用したらどうなるか?
「普通はそんなことしないよね」という常識は、攻撃者にとっては通用しません。「あり得ない入力」をあえてテストしてみる習慣をつけましょう。
C. 変更検知のためのヘッダーや監査ログの活用
リクエストの不正な改ざんや、短期間での大量のワークフロー実行を検知するために、アクセスログやWAF(Webアプリケーションファイアウォール)のカスタムルールを活用します。
例えば、以下のようなセキュリティヘッダーや、アプリケーション独自のカスタムヘッダーを使って、正当な画面遷移を経たリクエストであるかを検証するアプローチもあります。
# リクエストが特定の信頼できる画面遷移を経ていることを示すカスタムヘッダーの例
X-Requested-Workflow-Step: checkout_confirmation
※もちろん、これ単体では不十分ですが、多層防御の一部として有効です。
—
まとめ:一歩ずつ対策を学んでいきましょう!
ビジネスロジックの脆弱性は、自動化されたツールではなかなか見つからないため、「開発者やテスターの想像力」が最大の防御武器になります。
「もしこの入力欄に文字ではなく絵文字を入れたら?」
「もしこの決済ボタンを2秒おきに100回連打したら?」
そんな風に、ちょっと意地悪な視点を持って自分の作ったシステムを眺めてみることは、エンジニアとして最高に面白いスキルアップの第一歩です。
最初は難しく感じるかもしれませんが、日々の開発の中で「あ、ここ、ユーザーの入力をそのまま信じちゃいけないな」と気づける瞬間が増えていけば、確実にセキュリティの腕は上がっています。
一歩ずつ、安全で堅牢なシステム作りを楽しみながら学んでいきましょう!
コメント