Aikido

検知から予防へ:Zen実行時にIDOR脆弱性に対処する方法

執筆者
ハンス・オット

TL:DR

IDORは、マルチテナントSaaS企業がデータを漏洩させる最大の原因であり、通常はデプロイ後に発見されます。Aikido Zenはテナント分離を必須にします。Zenは、適切なSQLパーサー(Rustで記述)を使用して、ランタイム時にすべてのSQLクエリを解析し、すべてのクエリが正しいテナントでフィルタリングされているかを確認し、そうでない場合はエラーをスローします。開発者が誤ってテナント間アクセスを出荷することはなくなります。現在Node.jsで利用可能であり、Python、PHP、Go、Ruby、Java、.NETは近日中に対応予定です。

なぜIDORが今より危険なのか

IDORの脆弱性、別名Insecure Direct Object References(安全でない直接オブジェクト参照)は、マルチテナントアプリケーションにおいて最も一般的で危険な欠陥の一つです。これは、クエリがテナントによるフィルタリングを忘れたときに発生し、あるアカウントが別のアカウントのデータにアクセスすることを許してしまいます。

長い間、IDORは発見が困難でした。コードスキャンでは検出されず、多くの手作業を必要としたためです。そのため、多くのIDOR脆弱性は、費用がかかり手間のかかるペネトレーションテスト中、あるいはセキュリティ研究者がバグ報奨金プログラムを通じて発見するまで、発見されませんでした。

しかし状況は変化した。エージェント型セキュリティテストツールは、実際のユーザーのように振る舞い、ワークフローをクリックし、役割を切り替え、自動的にリソースへのアクセスを試みることができるようになった。これにより、IDOR脆弱性の検出が格段に容易になった。しかしこれは両刃の剣である:これらの欠陥が発見しやすくなった分、エクスプロイト容易になる。組織がIDORの検出だけでなく、その防止に注力すべき理由はここにある。

なぜ検出だけでは不十分なのか

検出の側面では、AikidoのAI PentestはすでにIDORの脆弱性を発見できます。これは、IDORがコードパターンだけでなく認可コンテキストの理解を必要とするため、従来のパターンベースのSASTツールでは信頼性高く実行できなかったことです。AI Pentestは実際のユーザーとして認証し、完全なワークフローを実行し、役割を横断してオブジェクト識別子を再利用することで、実際にエクスプロイト可能なIDORの欠陥を発見します。このため、当社のAI Pentest機能を利用する多くの組織は、主にIDORの発見に関心を持っています。

しかし、IDORの脆弱性を発見することは、問題の半分にすぎません。理想的には、そもそもそれらが導入されるのを防ぐべきです。この投稿では、オープンソースのインアプリファイアウォールであるZenにおけるIDOR保護について説明します。これは、実行時にすべてのSQLクエリを分析し、クエリにテナントフィルターが欠けている場合や、誤ったテナントIDを使用している場合にエラーをスローします。これにより、開発およびテスト中にバグを捕捉し、本番環境に到達する前に防ぎます。

多くのエンタープライズ環境では、特にセキュリティレビューやベンダー評価の際に、マルチテナンシーがどのように強制され、テナント間のデータアクセスがどのように防止されているかという質問が繰り返し問われます。

セキュリティチームとリーダーシップ層は、テナントの境界が慣習だけでなく、体系的に強制されていることについて、明確で技術的な保証を求めています。

クエリレベルでテナントスコープを自動的に検証するメカニズムを持つことは、直接的で信頼できる回答を提供します。これにより、「開発者がこれを覚えておくことに依存している」という議論から、「システムが自動的に強制する」という議論へと移行します。

セットアップの概要は次のとおりです。

import Zen from "@aikidosec/firewall";

// 1. Tell Zen which column identifies the tenant
Zen.enableIdorProtection({
  tenantColumnName: "tenant_id",
  excludedTables: ["users"],
});

// 2. Set the tenant ID per request (e.g., in middleware after authentication)
app.use((req, res, next) => {
  Zen.setTenantId(req.user.organizationId);
  next();
});

// 3. Optionally bypass for specific queries (e.g., admin dashboards)
const result = await Zen.withoutIdorProtection(async () => {
  return await db.query("SELECT count(*) FROM orders WHERE status = 'active'");
});

IDORの脆弱性とはどのようなものでしょうか?

アプリケーションにアカウント、組織、ワークスペース、またはチームがある場合、おそらく次のようなカラムがあります tenant_id 各アカウントのデータを分離しています。しかし、クエリがそのカラムでフィルタリングを忘れた場合、または誤った値でフィルタリングした場合、あるアカウントが別のアカウントのデータにアクセスできることを意味します。これがIDORの脆弱性です。  

簡単な例を挙げます。ユーザーの注文を返すエンドポイントがあるとします。

app.get("/orders/:orderId", async (req, res) => {
  const order = await db.query(
    "SELECT * FROM orders WHERE id = $1",
    [req.params.orderId]
  );

  res.json(order);
});

問題がわかりますか?「~」がありません。 tenant_id フィルターです。もしアリスが GET /orders/42 を送信し、注文42がボブのものである場合、アリスはボブの注文を取得してしまいます。これがIDORです。

修正は簡単で、「~」を追加します。 WHERE tenant_id = $2 句です。しかし、このバグは導入しやすく、見つけるのが困難です。特に、数十のファイルにわたる数百のクエリを持つ大規模なコードベースでは。フィルターを一つ見落とすだけで発生します。

IDORは広範なカテゴリです。URL操作を介して他のユーザーのファイルにアクセスしたり、所有権をチェックしないAPIエンドポイントなども含まれます。この投稿では、特にSQLのテナントフィルタリングのサブセットに焦点を当て、すべてのデータベースクエリが現在のテナントに適切にスコープされていることを確認します。IDOR全般についてさらに深く掘り下げるには、当社のIDORの脆弱性解説の投稿をご覧ください。
今日の状況

既存のバグを発見することに重点を置くセキュリティスキャンとは別に、新しいIDORが導入されるのを防ぐための他の方法があります。それはフレームワークレベルのライブラリとデータベースレベルの強制です。それぞれに長所と短所があります。

フレームワークレベルのライブラリ

いくつかのフレームワークには、現在のテナントにクエリを自動的にスコープするライブラリがあります。

  • Ruby on Railsacts_as_tenantは、ActiveRecordモデルに自動的なテナントスコープを追加します。acts_as_tenant(:account)を宣言すると、そのモデルに対するすべてのクエリが現在のテナントによってフィルタリングされます。
  • Djangodjango-multitenantは、DjangoのORMに対しても同様の機能を提供します。ミドルウェアで現在のテナントを設定すると、Product.objects.all()は自動的にテナントスコープされます。
  • Laravel: Tenancy for Laravelは、シングルデータベースとマルチデータベースの両方に対応したマルチテナンシーを、自動コンテキスト切り替え機能とともに提供します。
  • .NET / EF Core: グローバルクエリフィルター 適用できます WHERE tenant_id = X モデルレベルで自動的にすべてのクエリに適用されます。

これらのライブラリは、それぞれのORM内でうまく機能します。しかし、ORMの抽象化を介するクエリのみを保護するという制限があります。生のSQLクエリ、他のライブラリからのクエリ、または同じプロジェクト内で異なるクエリビルダーで構築されたクエリは、スコープの対象外となります。また、これらはオプトインであり、すべてのモデルにアノテーションを追加することを忘れてはならず、新しいモデルが気づかれずに漏れてしまう可能性があります。公平を期すために、 acts_as_tenant ~を備えています。 require_tenant テナントが設定されていない場合にエラーを発生させ、「テナント設定忘れ」のリスクを大幅に軽減する設定です。

また、微妙な落とし穴もあります。例えばRailsでは、 acts_as_tenant ~を追加することで機能します。 default_scope。もし開発者が~を呼び出すと、 Project.unscoped 別のdefault scope(例えば~のような)を削除するために、 アーカイブ済み フィルターを適用すると、テナントフィルターを含むすべてのdefault scopeが、エラーや警告なしに削除されます。Railsには~があります。 unscope (~なしで) d)で単一のスコープを慎重に削除する方法がありますが、そのためにはそもそもテナントスコープが存在することを知っている必要があります。多数の開発者が関わるコードベースでは、いずれ誰かが~を使用し、 unscoped、テナント境界が静かに消滅します。

データベースレベルでの強制適用

PostgreSQLの行レベルセキュリティ (RLS) は、データベースレベルでテナント分離を強制することで、さらに一歩進んだ対策を提供します。アプリケーションが各クエリに WHERE tenant_id = ? を追加することに依存するのではなく、Postgres自体にそれを強制させます。

-- 1. 各テーブルでRLSを有効にする
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- 2. ポリシーを作成する:セッション変数に一致する行のみ許可する
CREATE POLICY tenant_isolation ON projects
  FOR ALL
  USING (テナントID = current_setting('app.current_tenant_id')::uuid);

-- 3. クエリを実行する前に、リクエストごとにテナントコンテキストを設定する
SET app.current_tenant_id = 'aaaa-aaaa-aaaa';

-- これで、単純なSELECT文でもそのテナントの行のみが返されるようになりました
SELECT * FROM projects;

RLSは、ここで挙げたアプローチの中で最も強力な保証を提供します。生のSQLクエリであろうとORMクエリであろうと関係なく、データベースが強制します。そして、 acts_as_tenantとは異なり、セッション変数の設定を忘れた場合、すべてのデータが返されるのではなく、データが一切返されません。これははるかに安全なデフォルト動作です。

しかし、これには実際のトレードオフが伴います。RLSはエラーをスローせず、クエリはサイレントに返される行数を減らしたり、何も影響を与えなかったりします。これはすべてのデータを返すよりも安全ですが、デバッグを困難にします。 UPDATE が100行を変更するはずが、ポリシーの不一致によりサイレントに0行しか影響しなかった場合、「データが存在しない」場合と区別するのが困難です。

コネクションプーリングも複雑さを増します。RLSは SET とpgBouncerのステートメントまたはトランザクションプーリングモードでは正しく機能せず、誤ったテナントの行が返される可能性があります。これは本番環境でしか表面化しないかもしれません。

構造的な制限もあります。スーパーユーザーはすべてのポリシーを完全にバイパスし、ビューはデフォルトでRLSをバイパスするため、アプリケーションは非スーパーユーザーロールとして接続する必要があります。最後に、Postgres専用です。MySQL、開発用のSQLite、または他のデータストアをサポートする必要がある場合、セキュリティレイヤーはそれらに対応できません。

実用的な結論として、RLSはテナント分離のセーフティネットとして優れていますが、運用上の複雑さとデバッグの困難さから、すべてのチームにとってすぐに導入できるソリューションではありません。

Zenの役割

これらはすべて有効なアプローチであり、いずれかを使用している場合は素晴らしいことです。ZenのIDOR保護は、異なるシナリオのために設計されています。クエリがデータベースドライバーを介して、直接またはORMを介して実行され、データベース構成を変更したり、特定のフレームワークライブラリを採用したりすることなく、どのORM、クエリビルダー、または生のSQLパターンを使用しても機能するセーフティネットが必要な場合です。

正直に言うと、Zenにも独自のトレードオフがあります。 acts_as_tenantと同様に、各リクエストで setTenantId を呼び出す必要があります。忘れた場合、Zenはエラーをスローするため、サイレントではなく明示的に失敗しますが、これはリクエストごとの設定という点で同じ種類のものです。また、RLSとは異なり、Zenはアプリケーション内で実行されるクエリのみをカバーします。例えば、psqlやZenを使用しない別のサービスを介して誰かがデータベースに直接接続した場合、それらのクエリはチェックされません。

また、設計上、言語に依存しません。SQL分析エンジンはRustで書かれているため、Node.jsとGo向けにはWebAssemblyにコンパイルされ、他のエージェントがFFIを介して呼び出すネイティブライブラリも提供されます。IDOR保護は、Python、PHP、Go、Ruby、Java、および.NETエージェントにも順次提供される予定です。

ZenがIDORから保護する方法

Zenはアプリケーション内に配置され、リクエストを行っているユーザーに関する完全なコンテキストを持って、実行時にSQLクエリを分析します。

Rustで書かれた適切なSQLパーサー

ZenのIDOR保護の中核には、Rustのsqlparserクレート上に構築され、Node.jsおよびGo向けにWebAssemblyにコンパイルされた真のSQLパーサーがあります。これは、クエリの完全な抽象構文木(AST)を構築することでデータベースと同様の方法でSQLを解析し、その後ツリーを走査して以下を抽出します。

  • クエリが参照するテーブル(エイリアスを含む)
  • WHERE句内の等価フィルター
  • INSERT文に含まれるカラムと値

なぜ正規表現(regex)ではないのか?正規表現は、以下のような単純なクエリには問題なく機能します。 SELECT * FROM orders WHERE tenant_id = ?しかし、実際のアプリケーションでは、CTE、UNION、サブクエリ、エイリアス付きのJOINなど、正規表現ベースのアプローチでは対応が難しいあらゆる種類の有効なSQLが存在します。クエリが複雑になるにつれて、正規表現ベースの解析はますます脆弱になります。それが必ずしも間違っているわけではありませんが、保守が困難であり、予期せぬ挙動に遭遇しやすいです。

適切なパーサーは、これらすべてをすぐに処理します。また、DDLステートメント(CREATE TABLE, ALTER TABLE)、トランザクション制御(BEGIN, COMMIT, ROLLBACK)、およびセッションコマンド(SET, SHOW)。

内部での分析は次のようになります。次のクエリが与えられた場合:

SELECT * FROM orders
LEFT JOIN order_items ON orders.id = order_items.order_id
WHERE orders.tenant_id = $1
AND orders.status = 'active';

パーサーは以下を生成します。

[
  {
    "kind": "select",
    "tables": [
      { "name": "orders" },
      { "name": "order_items" }
    ],
    "filters": [
      { "table": "orders", "column": "tenant_id", "value": "$1" },
      { "table": "orders", "column": "status", "value": "active" }
    ]
  }
]

Zenは、クエリ内のすべてのテーブルにフィルターが適用されているかを確認します。 tenant_id、そしてそのフィルター値が現在のテナントと一致するかどうかを確認します。

同様のことが以下にも当てはまります。 INSERT, UPDATE、および DELETEZenは、テナントカラムが常に存在し、常に正しい値を持つことを保証します。これらは単にログに記録されるだけでなく、エラーとしてスローされます。IDORは外部からの攻撃ではなく開発者のバグであるため、本番環境に漏れるのではなく、開発およびテスト中に明確に表面化させることが重要です。

パフォーマンス

すべてのクエリでSQLを解析するのはコストがかかるように思えますが、実際には高速です。重要な洞察は、ほとんどのアプリケーションがプリペアドステートメントまたはパラメーター化クエリを使用していることです。SQL文字列は同じままで、パラメーター値のみが変更されます。そのため、 SELECT * FROM orders WHERE tenant_id = $1 AND status = $2 は一度解析され、その後の同じクエリの実行はすべてキャッシュヒットとなります。

Zenが新しいクエリ文字列を初めて検出すると、RustパーサーはASTを構築し、テーブルとフィルターを抽出します。それ以降は、キャッシュルックアップと、テナントIDと解決されたプレースホルダー値の比較のみが行われます。

パラメータ化クエリではなく文字列連結のように、SQL文字列に値を直接埋め込む場合、一意の文字列ごとに新しいパースが必要になります。しかし、そもそもそのような方法は避けるべきです。パラメータ化クエリはSQLインジェクションから保護し、IDORチェックも高速化します。

本番環境への道のり:自社製品の活用

Aikidoのいくつかの社内サービスにZenのIDOR保護をデプロイしました。これにより、処理が必要なエッジケースがすぐに明らかになりました。

トランザクションサポートは初期の段階で課題となりました。実際のアプリケーションでは BEGIN, COMMIT、および ROLLBACK、Zenはこれらをテナントフィルタリングを必要としない安全なステートメントとして認識する必要があり、エラーを発生させるべきではありませんでした。最初の社内デプロイで失敗するのを確認した後、この機能を迅速に追加しました。

共通テーブル式 (CTE) ももう一つの課題でした。例えば、次のようなCTEは WITH active AS (SELECT * FROM orders WHERE tenant_id = $1) 後続のクエリが参照する仮想テーブルを作成します。ZenはCTE名を追跡し、「実テーブル」リストから除外すると同時に、適切なフィルタリングのためにCTE本体を分析する必要がありました。

会社情報 withoutIdorProtection エスケープハッチも不可欠であることが判明しました。管理ダッシュボード、バックグラウンドジョブ、クロステナント分析など、すべてのクエリがテナントフィルタリングを必要とするわけではありません。当初は、 ignoreNextQuery アプローチを試しました。これは、クエリの前に特定の関数を呼び出すことで、次のSQLステートメントのチェックをスキップするものです。

Zen.ignoreNextQuery();
const result = await db.query("SELECT count(*) FROM orders");

これは実際には脆弱であることが判明しました。コネクションプールを使用する場合、特定のコネクションにおける「次のクエリ」が、スキップしようとしたクエリではない可能性があります。コールバックベースの withoutIdorProtection はスコープが明確です。IDOR保護はコールバックの期間中のみ無効になり、それ以外は無効になりません。

クラウド資産データを提供するAPIをどのように保護したか

早期に保護したサービスの一つは、プラットフォーム全体にクラウド資産データを提供する社内APIでした。

このAPIは、UI、バックグラウンドジョブ、および複数のセキュリティエンジンが顧客のインフラに関する情報を読み取る必要がある際に使用されます。システムの中核をなし、毎秒数千のリクエストを処理します。

プラットフォームは完全にマルチテナントであるため、厳格なテナント分離が不可欠です。すべてのクエリは正しい組織にスコープされる必要があり、開発者がすべてのコードパスで適切なフィルターを追加することを覚えておくことに頼ることはできません。

ZenがIDOR保護をネイティブでサポートする前は、クエリレベルでテナントスコープを強制するカスタム実装を使用していました。Zenがこの動作に対するファーストクラスサポートを導入した後、自社開発ソリューションから組み込み機能に移行しました。これにより、メンテナンスするコードが大幅に削減されました。

現在、Zenは、高負荷時でもクエリが現在のテナントに適切にスコープされていることを自動的に検証します。ZenのIDOR保護を導入した後も、目立ったパフォーマンスへの影響は見られませんでした。

検知と防止

Aikidoの AI Pentestは、実際の攻撃をシミュレートすることで、実行中のアプリケーションにおけるIDORの脆弱性を発見します。ZenのIDOR保護は、開発時に不足しているテナントフィルターを捕捉することで、それらがそもそも導入されるのを防ぎます。

これら両方で、両側面をカバーします。AI Pentestは既存のコードが安全であることを検証し、Zenは新しいコードが安全に保たれることを保証します。既にデプロイされているものを監査するにはAI Pentestを使用し、コード作成時にミスを捕捉するにはZenを使用します。

はじめに

IDOR保護は、Node.js向けの @aikidosec/firewallで本日よりご利用いただけます。開始するには、セットアップガイドをご確認ください。他の言語のサポートは近日中に提供されます!

共有:

https://www.aikido.dev/blog/zen-stops-idor-vulnerabilities

ニュースを購読する

4.7/5
誤検知にうんざりしていませんか?
10万人以上のユーザーと同様に Aikido をお試しください。
今すぐ始める
パーソナライズされたウォークスルーを受ける

10万以上のチームに信頼されています

今すぐ予約
アプリをスキャンして IDORs と実際の攻撃パスを検出します

10万以上のチームに信頼されています

スキャンを開始
AI がどのようにアプリをペンテストするかをご覧ください

10万以上のチームに信頼されています

テストを開始

今すぐ、安全な環境へ。

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要です。 | スキャン結果は32秒で表示されます。