Conseils de sécurité pour API Web ASP.NET 2 OData

par Mike Wasson

Cette rubrique décrit certains des problèmes de sécurité que vous devez prendre en compte lors de l’exposition d’un jeu de données via OData pour API Web ASP.NET 2 sur ASP.NET 4.x.

Sécurité EDM

La sémantique de requête est basée sur le modèle de données d’entité (EDM), et non sur les types de modèles sous-jacents. Vous pouvez exclure une propriété de l’EDM et elle ne sera pas visible par la requête. Par exemple, supposons que votre modèle inclut un type Employee avec une propriété Salary. Vous pouvez exclure cette propriété de l’EDM pour la masquer aux clients.

Il existe deux façons d’exclure une propriété de l’EDM. Vous pouvez définir l’attribut [IgnoreDataMember] sur la propriété dans la classe de modèle :

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

Vous pouvez également supprimer la propriété de l’EDM par programme :

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

Sécurité des requêtes

Un client malveillant ou naïf peut être en mesure de construire une requête qui prend beaucoup de temps à s’exécuter. Dans le pire des cas, cela peut perturber l’accès à votre service.

L’attribut [Interrogeable] est un filtre d’action qui analyse, valide et applique la requête. Le filtre convertit les options de requête en expression LINQ. Lorsque le contrôleur OData retourne un type IQueryable , le fournisseur LINQ IQueryable convertit l’expression LINQ en requête. Par conséquent, les performances dépendent du fournisseur LINQ utilisé, ainsi que des caractéristiques particulières de votre jeu de données ou schéma de base de données.

Pour plus d’informations sur l’utilisation des options de requête OData dans API Web ASP.NET, consultez Prise en charge des options de requête OData.

Si vous savez que tous les clients sont approuvés (par exemple, dans un environnement d’entreprise) ou si votre jeu de données est petit, les performances des requêtes peuvent ne pas être un problème. Sinon, vous devez tenir compte des recommandations suivantes.

  • Testez votre service avec différentes requêtes et profilez la base de données.

  • Activez la pagination pilotée par le serveur pour éviter de renvoyer un jeu de données volumineux dans une requête. Pour plus d’informations, consultez Paging pilotée par le serveur.

    // Enable server-driven paging.
    [Queryable(PageSize=10)]
    
  • Avez-vous besoin de $filter et de $orderby ? Certaines applications peuvent autoriser la pagination du client, à l’aide de $top et de $skip, mais désactiver les autres options de requête.

    // Allow client paging but no other query options.
    [Queryable(AllowedQueryOptions=AllowedQueryOptions.Skip | 
                                   AllowedQueryOptions.Top)]
    
  • Envisagez de limiter $orderby aux propriétés d’un index cluster. Le tri des données volumineuses sans index cluster est lent.

    // Set the allowed $orderby properties.
    [Queryable(AllowedOrderByProperties="Id,Name")] // Comma separated list
    
  • Nombre maximal de nœuds : la propriété MaxNodeCount sur [Interrogeable] définit le nombre maximal de nœuds autorisés dans l’arborescence de syntaxe $filter. La valeur par défaut est 100, mais vous pouvez définir une valeur inférieure, car la compilation d’un grand nombre de nœuds peut être lente. Cela est particulièrement vrai si vous utilisez LINQ to Objects (c’est-à-dire des requêtes LINQ sur une collection en mémoire, sans l’utilisation d’un fournisseur LINQ intermédiaire).

    // Set the maximum node count.
    [Queryable(MaxNodeCount=20)]
    
  • Envisagez de désactiver les fonctions any() et all(), car elles peuvent être lentes.

    // Disable any() and all() functions.
    [Queryable(AllowedFunctions= AllowedFunctions.AllFunctions & 
        ~AllowedFunctions.All & ~AllowedFunctions.Any)]
    
  • Si des propriétés de chaîne contiennent des chaînes volumineuses(par exemple, une description de produit ou une entrée de blog), envisagez de désactiver les fonctions de chaîne.

    // Disable string functions.
    [Queryable(AllowedFunctions=AllowedFunctions.AllFunctions & 
        ~AllowedFunctions.AllStringFunctions)]
    
  • Envisagez d’interdire le filtrage sur les propriétés de navigation. Le filtrage sur les propriétés de navigation peut entraîner une jointure, qui peut être lente, en fonction de votre schéma de base de données. Le code suivant montre un validateur de requête qui empêche le filtrage sur les propriétés de navigation. Pour plus d’informations sur les validateurs de requête, consultez Validation des requêtes.

    // 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");
        }
    }
    
  • Envisagez de restreindre $filter requêtes en écrivant un validateur personnalisé pour votre base de données. Par exemple, considérez ces deux requêtes :

    • Tous les films avec des acteurs dont le nom commence par « A ».

    • Tous les films sortis en 1994.

      À moins que les films ne soient indexés par des acteurs, la première requête peut nécessiter que le moteur de base de données analyse la liste complète des films. Alors que la deuxième requête peut être acceptable, en supposant que les films sont indexés par année de publication.

      Le code suivant montre un validateur qui autorise le filtrage sur les propriétés « ReleaseYear » et « Title », mais aucune autre propriété.

      // 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 général, déterminez les fonctions $filter dont vous avez besoin. Si vos clients n’ont pas besoin de l’expressivité complète de $filter, vous pouvez limiter les fonctions autorisées.