Guía de seguridad de ASP.NET Web API 2 OData

de Mike Wasson

En este tema se describen algunos de los problemas de seguridad que deben tenerse en cuenta al exponer un conjunto de datos a través de OData para ASP.NET Web API 2 en ASP.NET 4.x.

Seguridad de EDM

La semántica de las consultas utiliza Entity Data Model (EDM), no los tipos de modelo subyacentes. Si excluye cualquier propiedad de EDM, la consulta no la verá. Por ejemplo, supongamos que el modelo incluye un tipo Employee con una propiedad Salary. Es posible que desee excluir esta propiedad de Entity Data Model para que no la vean los clientes.

Hay dos maneras de excluir una propiedad de Entity Data Model. Puede establecer el atributo [IgnoreDataMember] en la propiedad de la clase de modelo:

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

La propiedad también se puede quitar del EDM mediante programación:

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

Seguridad de las consultas

Un cliente malintencionado o naïve puede construir una consulta que tarde mucho tiempo en ejecutarse. En el peor de los casos, esto puede interrumpir el acceso a su servicio.

El atributo [Queryable] es un filtro de acción que analiza, valida y aplica la consulta. El filtro convierte las opciones de consulta en una expresión LINQ. Cuando el controlador OData devuelve un tipoIQueryable, el proveedor LINQ de IQueryable convierte la expresión LINQ en una consulta. Por tanto, el rendimiento depende del proveedor LINQ que se usa, así como de las características específicas del conjunto de datos o del esquema de la base de datos.

Para más información sobre el uso de opciones de consulta de OData en ASP.NET Web API, consulte Compatibilidad con las opciones de consulta de OData.

Si sabe que todos los clientes son de confianza (por ejemplo, en un entorno empresarial) o si el conjunto de datos es pequeño, es posible que el rendimiento de las consultas no suponga un problema. De lo contrario, debe tener en cuenta las siguientes recomendaciones.

  • Pruebe el servicio con varias consultas y perfile la base de datos.

  • Habilite la paginación controlada por servidor para evitar devolver un conjunto de datos grande en una consulta. Para más información, consulte Configuración del servidor.

    // Enable server-driven paging.
    [Queryable(PageSize=10)]
    
  • ¿Necesita $filter y $orderby? Algunas aplicaciones pueden permitir la paginación de cliente mediante $top y $skip, pero deshabilitan las otras opciones de consulta.

    // Allow client paging but no other query options.
    [Queryable(AllowedQueryOptions=AllowedQueryOptions.Skip | 
                                   AllowedQueryOptions.Top)]
    
  • Considere la posibilidad de restringir $orderby a las propiedades de un índice agrupado. La ordenación de datos grandes sin un índice agrupado es un proceso lento.

    // Set the allowed $orderby properties.
    [Queryable(AllowedOrderByProperties="Id,Name")] // Comma separated list
    
  • Número máximo de nodos: la propiedad MaxNodeCount en [Queryable] establece el número máximo permitido en el árbol de sintaxis de $filter. El valor predeterminado es 100, pero si lo desea, puede establecer un valor inferior, ya que un gran número de nodos puede tardar en compilarse. Esto pasa especialmente cierto si usa LINQ to Objects (es decir, consultas LINQ en una colección en memoria, sin el uso de un proveedor LINQ intermedio).

    // Set the maximum node count.
    [Queryable(MaxNodeCount=20)]
    
  • Considere la posibilidad de deshabilitar las funciones any() y all(), ya que pueden ser lentas.

    // Disable any() and all() functions.
    [Queryable(AllowedFunctions= AllowedFunctions.AllFunctions & 
        ~AllowedFunctions.All & ~AllowedFunctions.Any)]
    
  • Si cualquier propiedad de cadena contiene cadenas grandes (por ejemplo, una descripción del producto o una entrada de blog), considere la posibilidad de deshabilitar las funciones de cadena.

    // Disable string functions.
    [Queryable(AllowedFunctions=AllowedFunctions.AllFunctions & 
        ~AllowedFunctions.AllStringFunctions)]
    
  • Considere la posibilidad de no permitir el filtrado por las propiedades de navegación. El filtrado por las propiedades de navegación puede dar lugar a una combinación, lo que puede ser lento, en función del esquema de la base de datos. El código siguiente muestra un validador de consultas que impide el filtrado por las propiedades de navegación. Para más información sobre los validadores de consultas, consulte Validación de consultas.

    // 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");
        }
    }
    
  • Considere la posibilidad de restringir las consultas de $filter escribiendo un validador personalizado para la base de datos. Por ejemplo, considere estas dos consultas:

    • Todas las películas con actores cuyo apellido comienza por "A".

    • Todas las películas estrenadas en 1994.

      Salvo que los actores indexen películas, la primera consulta podría requerir que el motor de base de datos examinara toda la lista de películas. Sin embargo la segunda consulta podría ser aceptable, siempre que las películas se indexen por año de estreno.

      En el código siguiente se muestra un validador que permite filtrar por las propiedades "ReleaseYear" y "Title", pero no por otras.

      // 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);
          }
      }
      
  • En general, piense en las funciones de $filter que necesita. Si los clientes no necesitan la expresividad completa de $filter, puede limitar las funciones permitidas.