--[ 2. Le saviez-vous ? La question ]----------------------------------- Ruby, Python, PHP et Perl (entre autres) sont des langages très déployés aujourd'hui, et qui disposent de fonctions permettant d'assainir les chemins d'accès aux fichiers et aux répertoires. Ces fonctions vont normaliser les chaines de caractères pour résoudre les '..', les liens symboliques, etc. pour ne retourner qu'une chaine correspondant au chemin absolu vers un fichier/répertoire sur le système de fichiers. Toutefois quelques nuances subsistent dans l'implémention de ces fonctions. Quel est l'impact sur la sécurité entre les commandes suivantes ? $> php -r "echo realpath('//../etc//passwd');" $> python -c "import os; print os.path.abspath('//../etc//passwd')" $> ruby -e "require 'pathname'; print Pathname.new('//../etc/passwd').realpath.to_s" $> perl -e "use Cwd 'abs_path'; print abs_path('//../etc/passwd');" Réponse au paragraphe 14. --[ 14. Le saviez-vous ? La réponse ]------------------------------------ Si vous avez exécuté ces commandes*, peut-être avez-vous constaté les résultats suivants : {{{ $> php -r "echo realpath('//../etc//passwd');" /etc/passwd $> python -c "import os; print os.path.abspath('//../etc//passwd')" //etc/passwd $> ruby -e "require 'pathname'; print Pathname.new('//../etc/passwd').realpath.to_s" /etc/passwd $> perl -e "use Cwd 'abs_path'; print abs_path('//../etc/passwd');" /etc/passwd }}} Python a laissé les deux slashes (//) de début de chaine, alors que les autres n'en ont retournés qu'un seul. Or il est courant de voir dans des codes sources que pour empêcher un utilisateur d'accéder à un fichier non autorisé, le texte entré sera filtré avec les fonctions comme realpath() ou abspath(), puis la chaine sera testée pour voir si elle correspond à un motif interdit. Considérons le petit code suivant : {{{ # read_file.py : exemple de code avec Python 2.7 import os, sys chaine_filtree = os.path.abspath(os.path.realpath(sys.argv[1])) if chaine_filtree == "/etc/passwd": print "Vous n'avez pas le droit de lire /etc/passwd" else : with open(chaine_filtree, 'r') as f: print f.read() }}} Comme notre premier test l'a montré, malgré l'assainissement à l'aide des fonctions realpath() et abspath(), la vérification sera défaite si l'utilisateur rentre comme premier argument '//etc/passwd'. {{{ $> python read_file.py ////etc/passwd Vous n'avez pas le droit de lire /etc/passwd $> python read_file.py ../../../..//etc/passwd Vous n'avez pas le droit de lire /etc/passwd $> ln -s /etc/passwd ./lien_vers_passwd && python read_file.py ./lien_vers_passwd Vous n'avez pas le droit de lire /etc/passwd $> python read_file.py //etc/passwd root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin [...] }}} Mais alors il s'agirait d'un bogue propre à Python ? Inutile de faire un rapport de bogue, car Ruby, PHP, Perl comme Python respectent la norme POSIX de représentation de chemins de fichiers[2][3] ! En effet, la norme [2] indique ceci: """ A pathname that begins with two successive slashes may be interpreted in an implementation-defined manner, although more than two leading slashes shall be treated as a single slash. """ La norme est donc "claire" sur ce point : il est laissé à l'appréciation des développeurs (ceux de Python, Ruby, PHP, etc.) de laisser les deux slashes en début de chaine ou de les tronquer en un seul. Toutefois, si plus de 2 slashes sont en début de chaine, ils seront tronqués en un unique (ex: '///etc/passwd' deviendra bien '/etc/passwd'). Par ailleurs, ceci se voit très bien dans le code source de Python [1] : {{{ def normpath(path): [...] # POSIX allows one or two initial slashes, but treats three or more # as single slash. if (initial_slashes and path.startswith('//') and not path.startswith('///')): initial_slashes = 2 [...] if initial_slashes: path = slash*initial_slashes + path return path or dot }}} Cette ambigüité sur les deux premiers slashes dans la chaine de caractères peut donc induire un contournement de filtrage dans le cas où le développeur n'a pas connaissance de cette nuance, et va uniquement filtrer sur un chemin de structure '/A/B/C', oubliant le cas '//A/B/C' valide pour certains langages. Conclusion : Les développeurs se doivent de bien faire attention aux spécificités du langage dans lequel ils programment, même si celui-ci s'accorde avec la norme POSIX. En effet, comme la preuve vient d'en être faite, la norme POSIX peut inclure parfois quelques points imprécis qui sont laissés à l'appréciation des développeurs des langages Python, Ruby et autres. * Testé avec Python 2.7.2, Ruby 1.8.7, Perl v5.12.4 et PHP 5.3.6 Références : [1] http://svn.python.org/view/python/trunk/Lib/posixpath.py?view=markup#l308 [2] http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11 [3] http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_266 -- Christophe Alladoum