Le deuxième billet de cette série consacrée aux problèmes de sécurité aborde deux problématiques distinctes : les accès concurrentiels et l’encapsulation.
Accès concurrentiels
C’est l’un des domaines les plus compliqués à appréhender dans le développement, que ce soit au niveau de la
conception, beaucoup d’entre nous étant mono-cerveau, ou au niveau des tests, où les conditions d’exécution en
parallélisme sont souvent difficiles à reproduire. La gestion des accès concurrentiels impacte différentes dimensions
de la qualité : fiabilité, performances, maintenance, mais aussi la sécurité. Voici quelques exemples.
La partage d’attributs
Une des problématiques récurrentes dans le développement Java ou C# est de savoir si un objet peut être utilisé de
manière concurrentielle ou non, c’est la notion de thread-safe. Cette information doit être disponible dans la documentation de la classe. Une mauvaise
utilisation peut conduire à des résultats imprévisibles. Via le Cockpit, nous ne comptons plus le nombre d’utilisations
non synchronisée des classes de formatage Java du package java.text, comme java.text.SimpleDateFormat,
avec des résultats qui doivent être fantaisistes selon les cas d’utilisation (oui en Java il faut toujours synchroniser le
formatage quand on recourt à ces classes).
Un exemple avec un impact direct pour la sécurité concerne l’utilisation de certains frameworks
webs. Beaucoup utilisent des pools d’objets pour gérer les instances qui répondent aux requêtes HTTP, ceci afin
d’améliorer les performances. Par exemple côté Java :
href= »http://struts.apache.org/1.x/userGuide/building_controller.html »>Struts V1,
href= »http://static.springsource.org/spring/docs/2.5.6/reference/mvc.html »>Spring MVC ou même les
href= »http://tomcat.apache.org/tomcat-5.5-doc/servletapi/javax/servlet/http/HttpServlet.html »>servlets. On
retrouve la même problématique côté C# avec des frameworks type
href= »http://blogs.msdn.com/benchr/archive/2008/09/03/does-asp-net-magically-handle-thread-safety-for-you.aspx »>ASP.NET
MVC.
Le problème est que ce mécanisme de pool n’est pas forcément bien mis en avant dans les documentations et que les
conséquences ne sont de toute façon pas toujours bien appréhendées par les développeurs. Le risque concret est tout
simplement de mélanger les informations provenant d’utilisateurs différents.
Un exemple avec une servlet (composant Java basique répondant à une requête HTTP) :
public class SomeServlet extends HttpServlet
{
private Account account;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
account = retrieveAccount(req);
doSomething();
}
protected void doSomething()
{
String name = account.getName();
...
}
}
Ici, on stocke les informations lues à partir de la requête HTTP dans un attribut de la servlet. Puis on effectue un
traitement via une autre méthode, traitement qui exploite ce même attribut. Le problème est que si une deuxième
requête arrive avant que la méthode doSomething() ne soit appelée, cet attribut sera écrasé avec les
informations liées à la deuxième requête. Car la servlet est par défaut utilisée via une seule instance partagée.
Il m’est arrivé d’intervenir sur un projet d’un client mondialement connu où les utilisateurs se plaignaient parfois
de perdre leur contexte et de voir affichées à l’écran des informations qui ne les concernaient pas. C’était
exactement ce cas ! Les conséquences n’ont pas été dramatiques car il s’agissait d’un intranet avec des informations
peu critiques, mais on imagine aisément ce que ceci peut donner dans des contextes plus sensibles.
Donc faites la chasse aux attributs dans les composants non thread-safe. Dans une application web,
les données doivent être mémorisées dans le contexte de la requête, de la session ou de l’application.
Les singletons
Même si l’utilisation de singletons est de plus en plus décriée au profit des mécanismes d’
href= »http://en.wikipedia.org/wiki/Dependency_injection »>injection de dépendances, c’est un pattern toujours
commun et qui paraît trivial à écrire. Et pourtant nos résultats d’analyse montrent que plus d’un projet sur deux
contient des singletons défectueux.
Un exemple en C#, où le singleton est créé dès le premier appel (lazy instantiation) :
public sealed class BalanceHistory
{
private static BalanceHistory INSTANCE;
private BalanceHistory() { }
public static BalanceHistory Instance
{
get
{
if (INSTANCE == null)
{
INSTANCE = new BalanceHistory();
}
return INSTANCE;
}
}
}
Si le getter getInstance() est appelé 2 fois simultanément, il est possible que le singleton soit recréé
deux fois : si les appels testent que le singleton est nul, ils provoquent chacun la création d’une instance, et les deux
appelants travaillent alors sur des singletons différents. Et le singleton créé en premier est perdu. Embêtant
s’il stocke des mouvement financiers…
Il existe de multiples solutions pour résoudre ce problème :
- créer l’instance à la déclaration :
private static BalanceHistory INSTANCE = new BalanceHistory();. C’est généralement la meilleure solution, car il faut bien comprendre que cette instantiation ne sera vraiment effective qu’au chargement de la classe, donc dans le cas d’un singleton, au premier appel dugetInstance()! - synchronizer la création de l’instance : via l’attribut
[MethodImplAttribute(MethodImplOptions.Synchronized)],ou via un
locksur un objet verrou dédié - utiliser un pattern basé sur les classes internes, cf. le pattern
href= »http://fr.wikipedia.org/wiki/Support_d%27initialisation_%C3%A0_la_demande »>Support d’initialisation à la
demande
- le pattern Double-checked locking, même s’il
cause parfois plus de problèmes qu’il n’en résout (nous le considérons comme une pratique interdite sur le Cockpit)
Programmation objet
La sécurisation de l’accès aux données est un des principes fondateurs de la programmation orientée objet (POO), mais
ça ne signifie pas pour autant que tous les problèmes sont résolus.
Accessibilité des données
La POO permet de protéger les données à l’aide de mots-clés déterminant leur visibilité par rapport au reste de l’application,
suivant la localisation des classes : private, protected, public, internal
(C#), extern (C#), … D’autres mots-clés permettent d’interdire la surcharge d’une classe ou d’une méthode :
final (Java) ou sealed (C#). Cette configuration d’accessibilité n’est pas triviale. Elle
nécessite de trouver le bon équilibre entre la sécurisation du code, une bonne évolutivité et une utilisation simple.
Concernant la visibilité des attributs de classe, le développeur doit ajouter des accesseurs pour leur donner une plus
grande visibilité en se gardant la possibilité d’effectuer des contrôles ou des altérations. Comme ce travail n’est
pas très gratifiant, que le code est alourdi par des méthodes (et propriétés en C#) qui ne font généralement rien
d’autre que affecter ou consulter les attributs, et que le développeur est par nature paresseux, ces attributs sont
régulièrement déclarés non privés.
De même concernant la surcharge des classes ou méthodes, le développeur se préoccupe rarement d’interdire les
surcharges, sans en mesurer les risques.
Ces risques peuvent être de deux ordres :
- des exploitations mal-intentionnées provenant de code tiers. Cette objection est souvent théorique, car les
mécanismes de chargement de classes ne permettent pas facilement ce type d’exploitation. Cependant, certaines
architectures basées sur des mécanismes de plugins ou d’injection de dépendances rendent ces failles exploitables
- des implémentations à l’origine bien intentionnées peuvent introduire des failles en surchargeant des fonctions critiques qui
devraient être protégées, par exemple une méthode de récupération de droits utilisateurs.
Il faut donc toujours de soucier de la visibilité de données ou de traitements critiques.
Exposition des données mutables
Un autre aspect difficile à gérer dans la POO et qui a un impact direct sur la sécurité concerne l’exposition
d’attributs mutables. Un objet mutable est un objet dont l’état peut être modifié après son initialisation, par
exemple un tableau, un StringBuilder mais aussi la plupart des
href= »http://en.wikipedia.org/wiki/Plain_Old_Java_Object »>POJO/
href= »http://en.wikipedia.org/wiki/Plain_Old_CLR_Object »>POCO, que l’on crée dans une application.
Le problème est qu’un accesseur de type get sur un attribut mutable ne donne pas qu’un accès en consultation,
mais permet à l’appelant de modifier l’état de cet attribut. Une liste de droits utilisateur peut ainsi être modifiée
facilement :
public class User
{
private Set<UserRole> roles = new HashSet<UserRole>();
...
public Set<UserRole> getRoles()
{
return roles;
}
}
public class MaliciousCode
{
public void someMethod()
{
User user = userRegistry.findUser("myUserId");
user.getRoles().add(UserRole.Admin);
}
}
Pour se prémunir de ce type de problèmes, plusieurs solutions défensives sont possibles :
- retourner aux appelant une copie de l’attribut :
public class User { public Set<UserRole> getRoles() { return new HashSet<UserRole>(roles); } }Dans certains cas, la copie doit être réalisée en profondeur : si l’attribut est une liste d’objets mutables,
chaque objet mutable de la liste clonée doit également être cloné. Le problème est que les appelants ne disposent
pas de l’instance originale : si celle-ci est mise à jour par ailleurs, leur instance clonée ne sera plus à jour.
- renvoyer un proxy encapsulant l’objet mutable et bloquant les méthodes de mise à jour. Par exemple :
public class User { public Set<UserRole> getRoles() { return new HashSet<UserRole>(roles) { @Override public boolean add(UserRole o) { throw new UnsupportedOperationException(); } @Override public boolean remove(Object o) { throw new UnsupportedOperationException(); } ... }; } }Toutes les méthodes de mise à jour doivent être surchargées. Par rapport à la solution précédente, l’intérêt est
que l’on peut finement contrôler les accès à chaque méthode et surtout que l’on conserve l’instance originale à
travers l’encapsulation du proxy.
Java propose d’ailleurs une API permettant de verrouiller simplement des collections en lecture seule à l’aide de
ce mécanisme, via la classe
java.util.Collections. Il suffit par exemple d’écrire :public class User { public Set<UserRole> getRoles() { return Collections.unmodifiableSet(roles); } }
Dans tous les cas, ces mécanismes doivent être réservés à des cas spécifiques où les données exposées sont critiques
afin de ne pas grever les performances inutilement avec des instantiations inutiles.
A suivre… Dans le prochain billet, on évoquera les problèmes liés à l’injection de code.
