Zen Firewall による IDOR 保護
IDORはInsecure Direct Object Reference(安全でない直接オブジェクト参照)の略です。これはアクセス制御に関するバグの一種で、データベースクエリが適切にスコープされていないために、あるアカウントが別のアカウントのデータにアクセスできてしまうものです。
マルチテナントのSaaSアプリケーションでは、データは通常、tenant_id、account_id、organization_idなどのカラムで分離されています。クエリがそのカラムでのフィルタリングを忘れたり、誤った値を使用したりすると、アカウント間でデータが漏洩する可能性があります。
Zen FirewallのIDOR Protectionは、実行時にSQLクエリを解析し、テナントのスコープ設定を自動的に強制します。クエリが安全でない場合、Zenは即座にエラーをスローします。
これにより、本番環境でのインシデント発生後ではなく、開発およびテストの段階でマルチテナント分離のバグを検出できます。
脆弱性そのものの詳しい説明については、ブログ記事をご覧ください:IDOR Vulnerabilities Explained
サポートされているデータベース
現在サポートされているドライバー:
- mysqlおよびmysql2経由のMySQL
- pg経由のPostgreSQL
- SQLite(better-sqlite3およびnode:sqliteパッケージ経由)
内部でこれらのドライバーを使用するORMやクエリビルダーはすべてサポートされています。例:Drizzle、Knex、Sequelize、TypeORM。
Prismaのように独自のクエリエンジンを使用するORMは、サポートされているドライバーアダプターを使用するよう設定しない限り、サポートされません。
ESMを使用している場合は、ESMに関する注意事項を確認してください。インストルメントされていないESMのサブ依存関係内のクエリは、Zenでチェックできません。
現在サポートされているgem:
mysql2およびtrilogy経由のMySQLpg経由のPostgreSQLsqlite3経由のSQLite
内部でこれらのgemを使用するORMやクエリビルダーはすべてサポートされています。例:ActiveRecord。
独自のデータベースエンジンを使用するORMは、サポートされているドライバーアダプターを使用するよう設定しない限り、サポートされません。
制限事項
Zenが保護する対象
有効にすると、Zenは関連するすべてのクエリについて以下を検証します:
- SELECT、UPDATE、DELETEで正しいテナントカラムによるフィルタリングを行っていること
- 正しいテナントIDの値を使用していること
- すべてのINSERTにテナントカラムが含まれていること
- INSERTでテナントカラムに正しいテナントIDが設定されていること
Zenがブロックする問題の例:
- テナントフィルターのないSELECTにより、あるアカウントが別のアカウントのデータを読み取れてしまう
- テナントのスコープ設定がないUPDATEまたはDELETE
- テナントカラムを省略したINSERT
- 誤ったテナントIDを設定するINSERT
IDOR保護は、ブロックモードか検出モードかに関係なく、違反時に常にErrorをスローします。テナントフィルターの欠落は外部からの攻撃ではなく、開発者のバグです。
仕組み
Zenはサポートされているデータベースドライバーをインストルメントし、実行時にSQLクエリを検査します。リクエストごとに、アクティブなテナントIDを設定します。その後、Zenは関連するすべてのクエリが以下を満たしていることを検証します:
- 設定されたテナントカラムを参照していること。
- リクエストに対して設定されたものと同じテナントIDを使用していること。
クエリがこれらの条件を満たさない場合、Zenはエラーをスローし、クエリの実行を阻止します。
セットアップ
起動時にIDOR保護を有効にする
アプリケーションの起動時にIDOR保護を有効にします
import Zen from "@aikidosec/firewall"; Zen.enableIdorProtection({ tenantColumnName: "tenant_id", excludedTables: ["users"], });設定オプション:
-
tenantColumnNameテーブル内でテナントを識別するカラム。例:tenant_id、account_id、organization_id。
-
excludedTables単一のテナントにスコープされていないため、チェック対象外とするテーブル。例:すべてのテナントのユーザーを含む共有のusersテーブル。
Aikido::Zen.config.idor_protection_enabled = true Aikido::Zen.config.idor_tenant_column_name = "tenant_id" Aikido::Zen.config.idor_excluded_table_names = ["users"]設定オプション:
-
idor_tenant_column_nameテーブル内でテナントを識別するカラム。例:tenant_id、account_id、organization_id。
-
idor_excluded_table_names単一のテナントにスコープされていないため、チェック対象外とするテーブル。例:すべてのテナントのユーザーを含む共有のusersテーブル。
-
リクエストごとにテナントIDを設定する
IDOR保護が有効な場合、すべてのリクエストにテナントIDが必要です。
リクエストのライフサイクルの早い段階で、通常は認証後のミドルウェアでsetTenantIdを呼び出してください。
import Zen from "@aikidosec/firewall"; app.use((req, res, next) => { // Retrieve the tenant ID from your authentication layer Zen.setTenantId(req.user.organizationId); next(); });リクエストに対してsetTenantIdが呼び出されていない場合、SQLクエリの実行時にZenはErrorをスローします。
例えば認証後のミドルウェアで、
Aikido::Zen.set_tenant_idを呼び出します:Aikido::Zen.set_tenant_id(1)リクエストに対して
Aikido::Zen.set_tenant_idが呼び出されていない場合、SQLクエリの実行時にZenはAikido::Zen::IDOR::Errorを発生させます。特定のクエリでIDORチェックをバイパスする
テナント横断の分析や社内向けの管理レポートなど、意図的にグローバルなクエリもあります。
特定のコードブロックでIDORチェックをスキップするには:
withoutIdorProtectionを使用します:
import Zen from "@aikidosec/firewall"; const result = await Zen.withoutIdorProtection(async () => { return await db.query( "SELECT count(*) FROM agents WHERE status = 'running'" ); });コールバック内で実行されたクエリのみがIDOR検証から除外されます。
Aikido::Zen.without_idor_protectionを使用します:result = Aikido::Zen.without_idor_protection do db.execute("SELECT count(*) FROM agents WHERE status = 'running'") endブロック内で実行されたクエリのみがIDOR検証から除外されます。
この機能は慎重に、テナント横断のアクセスが明示的に必要な場合にのみ使用してください。
トラブルシューティング
テナントフィルターの欠落
Zen IDOR protection: query on table 'orders' is missing a filter on column 'tenant_id'クエリが設定されたテナントカラムでフィルタリングしていません。これはSELECT、UPDATE、DELETEに適用されます。
例:
SELECT * FROM orders WHERE status = 'active';tenant_idでフィルタリングする条件を追加してください。
誤ったテナントIDの値
Zen IDOR protection: query on table 'orders' filters 'tenant_id' with value '456' but tenant ID is '123'クエリはテナントカラムでフィルタリングしていますが、その値がsetTenantId(Node.js)またはAikido::Zen.set_tenant_id(Ruby)で設定されたテナントIDと一致しません。
すべてのクエリが現在のリクエストコンテキストのテナントIDを使用していることを確認してください。
INSERTでのテナントカラムの欠落
Zen IDOR protection: INSERT on table 'orders' is missing column 'tenant_id'すべてのINSERTにはテナントカラムを含める必要があります。
INSERTでの誤ったテナントID
Zen IDOR protection: INSERT on table 'orders' sets 'tenant_id' to '456' but tenant ID is '123'INSERTにテナントカラムは含まれていますが、その値がアクティブなテナントIDと一致しません。
`setTenantId`呼び出しの欠落
Zen IDOR protection: setTenantId() was not called for this request. Every request must have a tenant ID when IDOR protection is enabled.データベースクエリが実行される前に、すべてのリクエストでsetTenantIdが呼び出されていることを確認してください。
常に許可されるステートメント
Zenは行データの読み取りまたは変更を行うステートメントのみをチェックします:
- SELECT
- INSERT
- UPDATE
- DELETE
以下は認識されますが、IDORエラーを発生させることはありません:
- CREATE TABLE、ALTER TABLE、DROP TABLEなどのDDLステートメント
- SET、SHOWなどのセッションコマンド
- BEGIN、COMMIT、ROLLBACKなどのトランザクションステートメント