ファイアウォールの先へ:モダンAPIにおけるMass AssignmentとIDORの防御

Security tutorial - IT technology blog
Security tutorial - IT technology blog

午前2時15分の呼び出し:Mass Assignmentの事後分析(ポストモーテム)

PagerDutyのアラートは単に鳴ったのではありません。悲鳴を上げたのです。午前2時15分、Datadogのダッシュボードは、3分足らずの間に450件もの未承認の「role(ロール)」変更が行われたことを示していました。ノートPCを開く頃には、あるトライアルユーザーがすでに自分自身を「superuser」に昇格させ、15,000件の顧客レコードの持ち出しを開始していました。この攻撃は、高度なゼロデイ攻撃や複雑なSQLインジェクションを使ったものではありません。プロフィール更新エンドポイントに隠れていた、単純なMass Assignment(大量割り当て)の脆弱性が原因でした。

Mass Assignmentは「情報の過剰な共有」問題だと考えてください。これは、アプリケーションが生のユーザー入力を受け取り、フィルタリングせずに内部のデータベースモデルに直接バインドしたときに発生します。もしバックエンドのロジックが User.update(req.body) のようになっているなら、実質的にすべてのAPI利用者にスキーマ全体への書き込み権限を与えていることになります。攻撃者は JSON ペイロードに "is_admin": true を追加するだけで、データベースは喜んでそれに従ってしまうのです。

IDOR(不適切な直接オブジェクト参照)は、同様に危険な「従兄弟」のような存在です。これは、アプリが特定のレコードを取得する際にユーザー入力を信頼してしまうことで発生します。もし /api/orders/1024 を /api/orders/1025 に差し替えて他人の非公開の請求書を閲覧できるなら、その認可ロジックは破綻しています。どちらの欠陥も、オブジェクトレベルのアクセス制御(OLAC)の失敗を意味します。OLACとは、単にユーザーが誰であるかを確認するだけでなく、そのユーザーがどの特定のデータに触れることができるかを検証する手法です。

アタックサーフェス(攻撃対象領域)の分解

私が対応したインシデントでは、開発者がMVP(最小限の機能を持つ製品)の開発速度を上げるためにモダンなORMを使用していました。「ユーザーはログインしているのだから、自分のプロフィールを更新できるはずだ」という論理は一見正しく思えました。しかし、これは認証(Authentication:本人確認)と認可(Authorization:特定のリソースに対する権限確認)の間の溝を無視していました。

セキュリティは、たとえ危機的な状況であっても基本から始まります。この修正のためにステージング環境を構築する際、私は toolcraft.app/ja/tools/security/password-generator にあるツールを使って新しいサーバーシークレットを生成しました。これは完全にブラウザ上で動作するため、機密性の高い文字列がネットワークに流れることはありません。プレッシャーのかかる場面でこそ、こうした小さくクリーンな習慣が功を奏します。

ラボ:脆弱なAPIの構築

漏れを直すには、どこから水が漏れているかを確認する必要があります。以下は、本番環境で見つかった欠陥を再現した、Node.jsの最小限の環境です。Node.jsがインストールされているマシンであれば、どこでも実行できます。

# 環境のセットアップ
mkdir security-lab && cd security-lab
npm init -y
npm install express

以下のコードで app.js を作成します。これは、セキュリティ負債を招きがちな「迅速に動かし、破壊する(move fast and break things)」という典型的なアプローチを象徴しています。

const express = require('express');
const app = express();
app.use(express.json());

// モックデータベース
let users = {
    "1": { id: 1, username: "alice", role: "user", bio: "こんにちは!" },
    "2": { id: 2, username: "bob", role: "user", bio: "セキュリティが大好きです。" }
};

// 脆弱:Mass Assignment
app.put('/api/profile', (req, res) => {
    const userId = "1"; // セッションユーザーのモック
    // 欠陥:req.bodyを盲目的にデータベースオブジェクトにマージしている
    Object.assign(users[userId], req.body);
    res.send({ message: "プロフィールを更新しました", user: users[userId] });
});

// 脆弱:IDOR
app.get('/api/orders/:id', (req, res) => {
    const orderId = req.params.id;
    // 欠陥:orderIdがリクエスト送信者のものであるかどうかのチェックがない
    res.send({ orderId: orderId, data: "IDの機密請求書データ: " + orderId });
});

app.listen(3000, () => console.log('脆弱なサーバーがポート3000で起動中'));

修正:オブジェクトレベルのアクセス制御の実装

これらの穴を塞ぐには、データの整合性よりも開発の利便性を優先するのをやめる必要があります。必要なのは、更新のための厳格なアローリスト(許可リスト)と、データアクセス時の所有権の検証の2点です。

DTO(データ転送オブジェクト)による更新の保護

req.body を直接データベースに触れさせてはいけません。代わりに、許可したいフィールドを明示的に抽出します。この単純な変更により、攻撃者が更新クエリに role や balance フィールドを注入することは不可能になります。

// 安全:手動のアローリスト作成
app.put('/api/profile/secure', (req, res) => {
    const userId = "1"; 
    const { bio, location } = req.body; // 安全なものだけを抽出する
    
    if (bio) users[userId].bio = bio;
    if (location) users[userId].location = location;
    
    res.send({ message: "更新に成功しました", user: users[userId] });
});

所有権ミドルウェアによるIDORの阻止

IDORを防ぐには、リソースへのすべてのリクエストをゲートで制御する必要があります。認証されたユーザーIDが、データベース内のリクエストされたリソースの owner_id と一致することを検証しなければなりません。

// 安全:所有権の検証
const verifyOwnership = (req, res, next) => {
    const currentUserId = 1; // JWTまたはセッションから取得
    const requestedOrderId = req.params.id;
    
    // 実環境の例:const order = await db.orders.findById(requestedOrderId);
    const orderOwnerId = 2; // 注文101はボブ(ID 2)のものと仮定する

    if (currentUserId !== orderOwnerId) {
        return res.status(403).json({ error: "アクセス拒否:あなたはこのリソースの所有者ではありません" });
    }
    next();
};

app.get('/api/orders/secure/:id', verifyOwnership, (req, res) => {
    res.send({ orderId: req.params.id, data: "安全な請求書データ" });
});

検証:Postmanを使用した防御のテスト

修正が完了したら、それが機能することを証明する必要があります。Postmanによる自動セキュリティテストは、将来のスプリントでこれらの脆弱性がコードベースに再混入するのを防ぐ最良の方法です。

Mass Assignment防御の検証

  1. http://localhost:3000/api/profile/secure の PUT エンドポイントをターゲットにします。
  2. ボディを JSON に設定します。
  3. 攻撃を試行します: { "bio": "Hacked", "role": "admin" }
  4. リクエストを送信します。

レスポンスでは bio が変更されているものの、 role は user のままであるはずです。APIは未承認のフィールドを正常に無視しました。

IDOR防御の検証

  1. http://localhost:3000/api/orders/secure/101 に GET リクエストを送信します。
  2. ミドルウェアが不一致(アリスがボブのデータを見ようとしている状態)をシミュレートするため、サーバーは 403 Forbidden を返す必要があります。

警戒すべき兆候(レッドフラグ)の監視

検知は予防と同じくらい重要です。誰かが境界を調査していることを知る必要があります。以下の2つの具体的なメトリクスを監視することをお勧めします。

  • 403 Forbiddenの急増: 単一のIPが60秒以内に20の異なるリソースIDにアクセスし、403エラーを発生させている場合、IDORの列挙スクリプトを実行している可能性があります。
  • スキーマ違反: ZodやJoiなどのライブラリを使用します。ユーザーがスキーマにないフィールドを送信したすべてのインスタンスをログに記録します。ログに permissions や is_admin が含まれているのは、アクティブな攻撃が行われている精度の高い指標です。

私は通常、これらのイベントをSlackアラートに流します。しきい値を超えた場合、WAFが自動的に接続を遮断します。これが、午前2時15分のポケベルの呼び出しを二度と起こさないようにする唯一の方法です。不変のログに permissions や is_admin が含まれているのは、アクティブな攻撃が行われている精度の高い指標です。

Share: