ASP.NET Web API 2 OData のセキュリティ ガイダンス

作成者: Mike Wasson

このトピックでは、ASP.NET 4.x で ASP.NET Web API 2 用の OData 経由でデータセットを公開する場合に考慮すべきセキュリティの問題について説明します。

EDM セキュリティ

クエリ セマンティクスは、基になるモデル型ではなく、エンティティ データ モデル (EDM) に基づいています。 プロパティは EDM から除外でき、クエリには表示されなくなります。 たとえば、Salary プロパティを持つ Employee 型がモデルに含まれているとします。 このプロパティを EDM から除外して、クライアントから非表示にすることができます。

EDM からプロパティを除外するには、2 つの方法があります。 モデル クラスのプロパティに [IgnoreDataMember] 属性を設定できます。

public class Employee
{
    public string Name { get; set; }
    public string Title { get; set; }
    [IgnoreDataMember]
    public decimal Salary { get; set; } // Not visible in the EDM
}

また、プログラムで EDM からプロパティを削除することもできます。

var employees = modelBuilder.EntitySet<Employee>("Employees");
employees.EntityType.Ignore(emp => emp.Salary);

クエリのセキュリティ

悪意のあるクライアントや素朴なクライアントは、実行に非常に長い時間がかかるクエリを構築できる可能性があります。 最悪の場合、サービスへのアクセスが中断される可能性があります。

[Queryable] 属性は、クエリを解析、検証、および適用するアクション フィルターです。 このフィルターは、クエリ オプションを LINQ 式に変換します。 OData コントローラーが IQueryable 型を返すと、IQueryable LINQ プロバイダーは LINQ 式をクエリに変換します。 そのため、パフォーマンスは、使用される LINQ プロバイダーや、データセットまたはデータベース スキーマの特性によって異なります。

ASP.NET Web API での OData クエリ オプションの使用に関する詳細については、「OData クエリ オプションのサポート」を参照してください。

すべてのクライアントが信頼できることがわかっている場合 (エンタープライズ環境など)、またはデータセットが小さい場合は、クエリのパフォーマンスが問題にならない可能性があります。 それ以外の場合は、次の推奨事項を考慮する必要があります。

  • さまざまなクエリを使用してサービスをテストし、DB をプロファイリングします。

  • 1 つのクエリで大きなデータ セットが返されないように、サーバー駆動型ページングを有効にします。 詳細については、「サーバー駆動型ページング」を参照してください。

    // Enable server-driven paging.
    [Queryable(PageSize=10)]
    
  • $filter と $orderby が必要ですか? 一部のアプリケーションでは、$top と $skip を使用してクライアントのページングが許可されますが、他のクエリ オプションは無効になります。

    // Allow client paging but no other query options.
    [Queryable(AllowedQueryOptions=AllowedQueryOptions.Skip | 
                                   AllowedQueryOptions.Top)]
    
  • クラスター化インデックスのプロパティに $orderby を制限することを検討してください。 クラスター化インデックスを使用せずに大きなデータを並べ替えるのには時間がかかります。

    // Set the allowed $orderby properties.
    [Queryable(AllowedOrderByProperties="Id,Name")] // Comma separated list
    
  • 最大ノード数: [Queryable]MaxNodeCount プロパティは、$filter 構文ツリーで許可されるノードの最大数を設定します。 既定値は 100 ですが、ノードが多いとコンパイルに時間がかかる可能性があるため、より小さい値を設定する必要がある場合があります。 これは、LINQ to Objects (つまり、中間 LINQ プロバイダーを使用せずにメモリ内のコレクションに対して実行する LINQ クエリ) を使用している場合に特に当てはまります。

    // Set the maximum node count.
    [Queryable(MaxNodeCount=20)]
    
  • any() 関数と all() 関数は低速になる可能性があり、無効にすることを検討してください。

    // Disable any() and all() functions.
    [Queryable(AllowedFunctions= AllowedFunctions.AllFunctions & 
        ~AllowedFunctions.All & ~AllowedFunctions.Any)]
    
  • 文字列プロパティに大きな文字列 (製品の説明やブログ エントリなど) が含まれている場合は、文字列関数を無効にすることを検討してください。

    // Disable string functions.
    [Queryable(AllowedFunctions=AllowedFunctions.AllFunctions & 
        ~AllowedFunctions.AllStringFunctions)]
    
  • ナビゲーション プロパティのフィルター処理を禁止することを検討してください。 ナビゲーション プロパティをフィルター処理すると、データベース スキーマによっては結合に時間がかかる場合があります。 次のコードは、ナビゲーション プロパティのフィルター処理を防ぐクエリ検証コントロールを示しています。 クエリ検証コントロールの詳細については、「クエリの検証」を参照してください。

    // Validator to prevent filtering on navigation properties.
    public class MyFilterQueryValidator : FilterQueryValidator
    {
        public override void ValidateNavigationPropertyNode(
            Microsoft.Data.OData.Query.SemanticAst.QueryNode sourceNode, 
            Microsoft.Data.Edm.IEdmNavigationProperty navigationProperty, 
            ODataValidationSettings settings)
        {
            throw new ODataException("No navigation properties");
        }
    }
    
  • データベース用にカスタマイズされた検証コントロールを記述して、$filter クエリを制限することを検討してください。 たとえば、次の 2 つのクエリを考えてみます。

    • 姓が "A" で始まる俳優が出演するすべての映画。

    • 1994 年にリリースされたすべての映画。

      映画が俳優によってインデックス付けされていない限り、最初のクエリでは、DB エンジンが映画の一覧全体をスキャンする必要がある場合があります。 一方、2 番目のクエリは、映画がリリース年別にインデックス付けされていると仮定した場合は受け入れられる可能性があります。

      次のコードは、"ReleaseYear" プロパティと "Title" プロパティをフィルター処理できる検証コントロールを示していますが、その他のプロパティは使用しません。

      // Validator to restrict which properties can be used in $filter expressions.
      public class MyFilterQueryValidator : FilterQueryValidator
      {
          static readonly string[] allowedProperties = { "ReleaseYear", "Title" };
      
          public override void ValidateSingleValuePropertyAccessNode(
              SingleValuePropertyAccessNode propertyAccessNode,
              ODataValidationSettings settings)
          {
              string propertyName = null;
              if (propertyAccessNode != null)
              {
                  propertyName = propertyAccessNode.Property.Name;
              }
      
              if (propertyName != null && !allowedProperties.Contains(propertyName))
              {
                  throw new ODataException(
                      String.Format("Filter on {0} not allowed", propertyName));
              }
              base.ValidateSingleValuePropertyAccessNode(propertyAccessNode, settings);
          }
      }
      
  • 一般に、必要な $filter 関数を検討してください。 クライアントが $filter を完全に表現する必要がない場合は、許可される関数を制限できます。