Avec la technologie ASP.Net MVC, pour protéger son site web des attaques de type Cross-Site Request Forgery (CSRF ou XSRF) nous avons à notre disposition le filtre ValidateAntiForgeryTokenAttribute, un attribut que nous pouvons utiliser pour décorer la plupart des actions de nos contrôleurs. Généralement cet attribut doit être placé sur les actions type POST. La règle veut que les actions type GET ne doivent en aucun cas entraîner des effets secondaires (modification ou suppression de données etc… ). Par conséquent cet attribut est utile uniquement pour les actions POST.
Au vu du nombre d’actions qui peuvent exister dans une application web et nécessiter l’utilisation de cet attribut, un oubli peut vite arriver. D’ailleurs il est possible de mettre en place une règle FxCop pour détecter ces oublis. Cette solution permet d’être sûr qu’aucune action POST n’est passée entre les mailles du filet mais elle n’empêche pas au développeur de devoir quand même placer cet attribut sur les actions concernées pour être sûr de ne pas casser la build suite aux avertissements/erreurs FxCop remontés après son commit.
Le présent billet a pour but de fournir une solution permettant d’appliquer de façon globale cet attribut sur toutes les actions POST et aussi avoir la possibilité de négliger les actions sur lesquelles il serait inutile de l’avoir.
Il faut savoir que rien ne nous empêche de placer ValidateAntiForgeryTokenAttribute au niveau de chaque contrôleur mais cette solution pose problème vu que cet attribut ne permet pas d’appliquer le filtre uniquement sur les actions POST et esquiver les GET. Si vous appliquez ce filtre sur les actions GET vous recevrez un message d’erreur à l’exécution comme quoi le token est absent. Evidemment cela n’a aucun sens pour les actions GET. Cette difficulté de filtrage est aussi présente lorsqu’on applique le filtre da façon globale en utilisant le code ci-dessous :
public static void RegisterFilters(GlobalFilterCollection filters)
{
filters.Add(new ValidateAntiForgeryTokenAttribute());
}
Ce problème avec les actions GET ne se pose pas si le ou les contrôleurs contiennent uniquement des actions type POST ce qui est loin d’être le cas dans toutes les applications web dignes de ce nom.
On peut avoir l’idée de tout simplement créer un filtre personnalisé dérivé de ValidateAntiForgeryTokenAttribute sauf que cela est impossible vu que qu’elle est définie avec le mot-clé sealed (elle est fermée donc impossible d’en dériver).
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
{
// [...]
}
La solution permettant d’appliquer la protection anti CSRF uniquement à toutes nos actions POST est de passer par un fournisseur de filtres. Ce fournisseur de filtres sera interrogé à chaque fois qu’une action doit être exécutée pour renvoyer la liste de filtres additionnels qu’il faudra appliquer à l’action en cours.
Pour créer un fournisseur de filtres il faut :
1. Créer une classe implémentant l’interface IFilterProvider. Cette interface définit un seul contrat GetFilters.
public interface IFilterProvider
{
IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
}
2. Ajouter une instance de ce fournisseur de filtres dans la collection statique Providers de la classe FilterProviders.
public static void RegisterFilters(GlobalFilterCollection filters)
{
FilterProviders.Providers.Add(new ValidateAntiForgeryTokenProvider());
}
Ci-dessous le code de notre fournisseur de filtres.
public class ValidateAntiForgeryTokenProvider : IFilterProvider
{
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
if (!controllerContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.InvariantCultureIgnoreCase))
{
return Enumerable.Empty<Filter>();
}
return new List<Filter>
{
new Filter(new ValidateAntiForgeryTokenAttribute(), FilterScope.Global, null)
};
}
}
Dans l’implémentation de la méthode GetFilters, nous vérifions qu’il s’agit bien d’une action type POST. Si ce n’est pas le cas alors nous renvoyons tout simplement une collection vide. S’il s’agit du type attendu alors nous renvoyons une liste avec une seule instance de la classe Filter recevant en paramètre de son constructeur une instance de l’attribut ValidateAntiForgeryTokenAttribute.
Dans l’état actuel du code de notre fournisseur de filtres, toutes les actions type POST seront impactées. N’oublions pas que la protection contre les attaques CSRF n’est utile que pour les actions sensibles et nécessitant une authentification de l’utilisateur. Il peut exister des actions POST telle que l’action pour le login où la protection anti-CSRF n’est pas utile mais il est recommandé dans le cas où la connexion à votre application se déroule comme le montre un des exemples se trouvant dans ce fichier PDF. Cela ne pose aucun problème d’avoir à appliquer la protection sur toutes nos actions POST mis à part que cela oblige le développeur à devoir placer dans les vues associées un champ caché contenant le token et cela se fait assez facilement avec Razor en utilisant la seule ligne de code suivante dans notre vue :
@Html.AntiForgeryToken();
Cependant si vous voulez tout de même ne pas appliquer cette protection sur les actions non-sensibles alors l’implémentation de notre fournisseur ressemblera au code ci-dessous :
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
IEnumerable<Filter> filters = Enumerable.Empty<Filter>();
if (!controllerContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.InvariantCultureIgnoreCase))
{
return filters;
}
if (actionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false) || actionDescriptor.IsDefined(typeof(DisableValidateAntiForgeryTokenAttribute), false))
{
return filters;
}
filters = new List<Filter>
{
new Filter(new ValidateAntiForgeryTokenAttribute(), FilterScope.Global, null)
};
return filters;
}
Avec cette nouvelle implémentation, toute action sur laquelle on aura placé l’attribut AllowAnonymous ou DisableValidateAntiForgeryTokenAttribute sera ignorée par notre fournisseur de filtres. Pas besoin de détails sur l’attribut AllowAnonymous si vous avez déjà utilisé l’authentification ASP.net Identity. Le code de l’attribut DisableValidateAntiForgeryTokenAttribute est tout simplement une classe vide dérivée de la classe de base Attribute :
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class DisableValidateAntiForgeryTokenAttribute : Attribute
{
}
Et voilà c’est tout :)
Le code complet de notre fournisseur de filtres est le suivant
public class ValidateAntiForgeryTokenProvider : IFilterProvider
{
/// <summary>
/// Returns an enumerator that contains all the <see cref="T:System.Web.Mvc.IFilterProvider" /> instances in the service locator.
/// </summary>
/// <param name="controllerContext">The controller context.</param>
/// <param name="actionDescriptor">The action descriptor.</param>
/// <returns>
/// The enumerator that contains all the <see cref="T:System.Web.Mvc.IFilterProvider" /> instances in the service locator.
/// </returns>
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
IEnumerable<Filter> filters = Enumerable.Empty<Filter>();
if (!controllerContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.InvariantCultureIgnoreCase))
{
return filters;
}
if (actionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false) || actionDescriptor.IsDefined(typeof(DisableValidateAntiForgeryTokenAttribute), false))
{
return filters;
}
filters = new List<Filter>
{
new Filter(new ValidateAntiForgeryTokenAttribute(), FilterScope.Global, null)
};
return filters;
}
}
La problématique décrite dans ce billet nous montre qu’il est parfois difficile d’appliquer certains filtres avec une portée beaucoup plus générale sans impacter certaines actions qui ne seraient pas concernées. ValidateAntiForgeryTokenAttribute n’est nécessaire que pour les requêtes POST et cela oblige le développeur à répéter son utilisation autant de fois qu’il a des actions à protéger contre les attaques XSRF. La solution proposée est de passer par un fournisseur de filtres. Pour chaque action choisie par la route et avant son exécution, le fournisseur est interrogé pour avoir une liste de filtres à appliquer. Ainsi le filtre ValidateAntiForgeryTokenAttribute est inclus dans cette liste uniquement s’il a un sens pour l’action concernée.