Orientações de Segurança para ASP.NET Web API 2 OData

por Mike Wasson

Este tópico descreve algumas das questões de segurança que deve considerar ao expor um conjunto de dados através do OData para ASP.NET Web API 2 na ASP.NET 4.x.

Segurança EDM

A semântica da consulta baseia-se no modelo de dados da entidade (EDM), não nos tipos de modelo subjacentes. Podes excluir uma propriedade do EDM e ela não será visível para a consulta. Por exemplo, suponha que o seu modelo inclui um tipo de Funcionário com uma propriedade de Salário. Talvez queira excluir esta propriedade do EDM para a ocultar dos clientes.

Existem duas formas de excluir uma propriedade do EDM. Pode definir o atributo [IgnoreDataMember] na propriedade da classe 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
}

Também pode remover a propriedade do EDM de forma programática:

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

Segurança de Consultas

Um cliente malicioso ou ingénuo pode ser capaz de construir uma consulta que demora muito tempo a ser executada. No pior dos casos, isto pode perturbar o acesso ao seu serviço.

O atributo [Queryable] é um filtro de ação que analisa, valida e aplica a consulta. O filtro converte as opções de consulta numa expressão LINQ. Quando o controlador OData retorna um tipo IQueryable , o fornecedor IQueryable LINQ converte a expressão LINQ numa consulta. Portanto, o desempenho depende do fornecedor LINQ utilizado e também das características particulares do seu conjunto de dados ou esquema de base de dados.

Para mais informações sobre o uso de opções de consulta OData na API Web ASP.NET, consulte Suporte a Opções de Consulta OData.

Se souber que todos os clientes são de confiança (por exemplo, num ambiente empresarial), ou se o seu conjunto de dados for pequeno, o desempenho das consultas pode não ser um problema. Caso contrário, deve considerar as seguintes recomendações.

  • Teste o seu serviço com várias consultas e faça o perfil da base de dados.

  • Ative a paginação dirigida ao servidor, para evitar devolver um grande conjunto de dados numa única consulta. Para mais informações, consulte Paginação Controlada pelo Servidor.

    // Enable server-driven paging.
    [Queryable(PageSize=10)]
    
  • Precisas de $filter e $orderby? Algumas aplicações podem permitir a paginação do cliente, usando $top e $skip, mas desativar as outras opções de consulta.

    // Allow client paging but no other query options.
    [Queryable(AllowedQueryOptions=AllowedQueryOptions.Skip | 
                                   AllowedQueryOptions.Top)]
    
  • Considere restringir $orderby a propriedades num índice agrupado. Ordenar dados grandes sem um índice agrupado é lento.

    // Set the allowed $orderby properties.
    [Queryable(AllowedOrderByProperties="Id,Name")] // Comma separated list
    
  • Número máximo de nós: A propriedade MaxNodeCount em [Queryable] define o número máximo de nós permitidos na árvore sintáctica $filter. O valor padrão é 100, mas pode querer definir um valor mais baixo, porque um grande número de nós pode ser lento a compilar. Isto é particularmente verdade se estiver a usar LINQ para Objetos (ou seja, consultas LINQ numa coleção na memória, sem o uso de um fornecedor intermédio de LINQ).

    // Set the maximum node count.
    [Queryable(MaxNodeCount=20)]
    
  • Considere desativar as funções any() e all(), pois estas podem ser lentas.

    // Disable any() and all() functions.
    [Queryable(AllowedFunctions= AllowedFunctions.AllFunctions & 
        ~AllowedFunctions.All & ~AllowedFunctions.Any)]
    
  • Se alguma propriedade de cadeia contiver cadeias grandes — por exemplo, uma descrição de produto ou uma entrada de blog — considere desativar as funções de cadeia.

    // Disable string functions.
    [Queryable(AllowedFunctions=AllowedFunctions.AllFunctions & 
        ~AllowedFunctions.AllStringFunctions)]
    
  • Considere não permitir filtragem nas propriedades de navegação. Filtrar as propriedades de navegação pode resultar numa junção, que pode ser lenta, dependendo do esquema da sua base de dados. O código seguinte mostra um validador de consultas que impede a filtragem das propriedades de navegação. Para mais informações sobre validadores de consultas, consulte Validação 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 restringir $filter consultas escrevendo um validador personalizado para a sua base de dados. Por exemplo, considere estas duas consultas:

    • Todos os filmes com atores cujo apelido começa por 'A'.

    • Todos os filmes lançados em 1994.

      A menos que os filmes sejam indexados por atores, a primeira consulta pode exigir que o motor de base de dados escaneie toda a lista de filmes. Enquanto a segunda pergunta pode ser aceitável, assumindo que os filmes estão indexados por ano de lançamento.

      O código seguinte mostra um validador que permite filtrar as propriedades "ReleaseYear" e "Title", mas não outras propriedades.

      // 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);
          }
      }
      
  • De um modo geral, considere quais funções $filter necessita. Se os seus clientes não precisarem da expressividade total do $filter, pode limitar as funções permitidas.