Security guidance for ASP.NET Core Web API OData

By Mike Wasson, FIVIL and Rick Anderson

This page describes some of the security issues that you should consider when exposing a dataset through OData for ASP.NET Core Web API.

Query security

Suppose your model includes an Employee type with a Salary property. You might want to exclude this property to hide it from clients. Properties can be excluded with [IgnoreDataMember]:

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Title { get; set; }

    // Requires using System.Runtime.Serialization;
    [IgnoreDataMember]
    public decimal Salary { get; set; }
}

A malicious or naive client can construct a query that:

  • Takes significant system resources. Such a query can disrupt your service.
  • Leaks sensitive information from a clever join.

The [EnableQuery] attribute is an action filter that parses, validates, and applies the query. The filter converts the query options into a LINQ expression. When the controller returns an System.Linq.IQueryable type, the IQueryable LINQ provider converts the LINQ expression into a query. Therefore, performance depends on the LINQ provider that is used, and on the particular characteristics of the dataset or database schema.

If all clients are trusted (for example, in an enterprise environment), or if the dataset is small, query performance might not be an issue. Otherwise, consider the following recommendations:

  • Test your service with anticipated queries and profile the DB.

  • Enable server-driven paging to avoid returning a large data set in one query.

    // Enable server-driven paging. Requires using Microsoft.AspNet.OData;
    [EnableQuery(PageSize = 10)]
    
  • Does the app require $filter and $orderby? Some apps might allow client paging, using $top and $skip, but disable the other query options.

    // Allow client paging but no other query options.
    // Requires using Microsoft.AspNet.OData.Query;
    [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Skip |
                                       AllowedQueryOptions.Top)]
    
  • Consider restricting $orderby to properties in a clustered index. Sorting large data without a clustered index is resource-intensive.

    [EnableQuery(AllowedOrderByProperties = "Id,Name")] // Comma separated list
    
  • Maximum node count: The MaxNodeCount property on [EnableQuery] sets the maximum number nodes allowed in the $filter syntax tree. The default value is 100, but you may want to set a lower value. A large number of nodes can be slow to compile. This is important when using LINQ to Objects.

    [EnableQuery(MaxNodeCount = 20)]
    
  • Consider disabling the any and all functions, as these can be resource-intensive:

    // Disable any() and all() functions.
    [EnableQuery(AllowedFunctions = AllowedFunctions.AllFunctions &
                ~AllowedFunctions.All & ~AllowedFunctions.Any)]
    
  • If any string properties contain large strings—for example, a product description or a blog entry—consider disabling the string functions.

    // Disable string functions.
    [EnableQuery(AllowedFunctions = AllowedFunctions.AllFunctions &
                              ~AllowedFunctions.AllStringFunctions)]
    
  • Consider disallowing filtering on navigation properties. Filtering on navigation properties can result in a join. Joins can be slow, depending on the database schema. The following code shows a query validator that prevents filtering on navigation properties.

    // Validator to prevent filtering on navigation properties.
    public class MyFilterNavPropQueryValidator : FilterQueryValidator
    {
        
        public override void ValidateNavigationPropertyNode(
        QueryNode sourceNode,
        IEdmNavigationProperty navigationProperty,
        ODataValidationSettings settings)
        {
            throw new ODataException("Filtering on navigation properties prohibited");
        }
    
        public MyFilterNavPropQueryValidator(DefaultQuerySettings defaultQuerySettings) 
                                                           : base(defaultQuerySettings)
        {
    
        }
    }
    
  • Consider restricting $filter queries by writing a validator that is customized for the database. For example, consider these two queries:

    • All movies with actors whose last name starts with A.

    • All movies released in 1994.

      Unless movies are indexed by actors, the first query might require the DB engine to scan the entire list of movies. Whereas the second query might be acceptable, assuming movies are indexed by release year.

      The following code shows a validator that allows filtering on the ReleaseYear and Title properties but no other properties.

      // 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);
          }
      
          public MyFilterQueryValidator(DefaultQuerySettings defaultQuerySettings)
                                                      : base(defaultQuerySettings)
          {
      
          }
      }
      
  • In general, consider which $filter functions are required. If clients don't need the full expressiveness of $filter, limit the allowed functions.

EDM security

The query semantics are based on the Entity Data Model (EDM), not the underlying model types. You can exclude a property from the EDM and it will not be visible to the query. For example, suppose your model includes an Employee type with a Salary property. You might want to exclude this property from the EDM to hide it from clients.

Properties can be excluded with [IgnoreDataMember] or programmatically with the EDM. The following code removes the Salary property from the EDM programmatically:

private static IEdmModel GetEdmModel()
{
    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
    var employees = builder.EntitySet<Employee>("Employees");
    employees.EntityType.Ignore(emp => emp.Salary);
    return builder.GetEdmModel();
}