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에서 속성을 제외하는 방법에는 두 가지가 있습니다. 모델 클래스의 속성에서 [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를 프로파일합니다.

  • 하나의 쿼리에서 큰 데이터 집합을 반환하지 않도록 서버 기반 페이징을 사용하도록 설정합니다. 자세한 내용은 서버 기반 페이징을 참조하세요.

    // 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 쿼리를 제한하는 것이 좋습니다. 예를 들어 다음 두 쿼리를 고려합니다.

    • 이름이 'A'로 시작하는 행위자가 있는 모든 영화.

    • 1994년에 개봉된 모든 영화.

      행위자가 영화를 인덱싱하지 않는 한 첫 번째 쿼리는 DB 엔진이 영화 전체 목록을 검사해야 할 수 있습니다. 영화가 릴리스 연도별로 인덱싱된다고 가정하면 두 번째 쿼리가 허용될 수 있습니다.

      다음 코드에서는 "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 전체 표현력이 필요하지 않은 경우 허용되는 함수를 제한할 수 있습니다.