web-dev-qa-db-fra.com

Comment vérifier si un répertoire est un sous-répertoire d'un autre répertoire

J'aime écrire un système de template en Python, qui permet d'inclure des fichiers.

par exemple.

 Ceci est un modèle 
 Vous pouvez inclure en toute sécurité des fichiers avec safe_include`othertemplate.rst` 

Comme vous le savez, l'inclusion de fichiers peut être dangereuse. Par exemple, si j'utilise le système de modèles dans une application Web qui permet aux utilisateurs de créer leurs propres modèles, ils pourraient faire quelque chose comme

 Je veux vos mots de passe: safe_include`/etc/password` 

Par conséquent, je dois restreindre l'inclusion de fichiers à des fichiers qui se trouvent par exemple dans un certain sous-répertoire (par exemple /home/user/templates)

La question est maintenant: comment puis-je vérifier si /home/user/templates/includes/inc1.rst se trouve dans un sous-répertoire de /home/user/templates?

Le code suivant fonctionnerait-il et serait-il sécurisé?

import os.path

def in_directory(file, directory, allow_symlink = False):
    #make both absolute    
    directory = os.path.abspath(directory)
    file = os.path.abspath(file)

    #check whether file is a symbolic link, if yes, return false if they are not allowed
    if not allow_symlink and os.path.islink(file):
        return False

    #return true, if the common prefix of both is equal to directory
    #e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b
    return os.path.commonprefix([file, directory]) == directory

Aussi longtemps que allow_symlink est faux, il devrait être sécurisé, je pense. Autoriser les liens symboliques, bien sûr, rendrait non sûr si l'utilisateur est capable de créer de tels liens.

PDATE - Solution Le code ci-dessus ne fonctionne pas, si les répertoires intermédiaires sont des liens symboliques. Pour éviter cela, vous devez utiliser realpath au lieu de abspath.

PDATE: ajout d'un répertoire de fin/vers pour résoudre le problème avec commonprefix () Reorx l'a souligné.

Cela fait également allow_symlink inutile car les liens symboliques sont étendus à leur destination réelle

import os.path

def in_directory(file, directory):
    #make both absolute    
    directory = os.path.join(os.path.realpath(directory), '')
    file = os.path.realpath(file)

    #return true, if the common prefix of both is equal to directory
    #e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b
    return os.path.commonprefix([file, directory]) == directory
41
Simon

os.path.realpath (path): renvoie le chemin canonique du nom de fichier spécifié, en éliminant tous les liens symboliques rencontrés dans le chemin (s'ils sont pris en charge par le système d'exploitation).

Utilisez-le sur le nom du répertoire et du sous-répertoire, puis vérifiez que ce dernier commence par ancien.

11
blaze

Le module pathlib de Python 3 rend cela simple avec son attribut Path.parents . Par exemple:

from pathlib import Path

root = Path('/path/to/root')
child = root / 'some' / 'child' / 'dir'
other = Path('/some/other/path')

Ensuite:

>>> root in child.parents
True
>>> other in child.parents
False
40
jme

Problèmes avec de nombreuses méthodes suggérées

Si vous voulez tester la filiation d'un répertoire avec une comparaison de chaînes ou os.path.commonprefix méthodes, celles-ci sont sujettes à des erreurs avec des chemins de nom similaire ou des chemins relatifs. Par exemple:

  • /path/to/files/myfile serait affiché comme un chemin enfant de /path/to/file en utilisant de nombreuses méthodes.
  • /path/to/files/../../myfiles ne serait pas affiché en tant que parent de /path/myfiles/myfile par de nombreuses méthodes. En fait, ça l'est.

Le réponse précédente de Rob Dennis fournit un bon moyen de comparer la filiation du chemin sans rencontrer ces problèmes. Python 3.4 a ajouté le module pathlib qui peut effectuer ce type d'opérations de chemin d'une manière plus sophistiquée, éventuellement sans référencer le système d'exploitation sous-jacent. Jme a décrit dans n autre réponse précédente comment utiliser pathlib dans le but de déterminer avec précision si un chemin est un enfant d'un autre. Si vous préférez ne pas utiliser pathlib (vous ne savez pas pourquoi, c'est plutôt bien) puis Python 3.5 a introduit une nouvelle méthode basée sur le système d'exploitation dans os.path qui vous permet d'effectuer des vérifications de chemin d'accès parent-enfant d'une manière similaire précise et sans erreur avec beaucoup moins de code.

Nouveau pour Python 3.5

Python 3.5 a introduit la fonction os.path.commonpath. Il s'agit d'une méthode spécifique au système d'exploitation sur lequel le code s'exécute. Vous pouvez utiliser commonpath de la manière suivante pour déterminer avec précision la filiation du chemin:

def path_is_parent(parent_path, child_path):
    # Smooth out relative path names, note: if you are concerned about symbolic links, you should use os.path.realpath too
    parent_path = os.path.abspath(parent_path)
    child_path = os.path.abspath(child_path)

    # Compare the common path of the parent and child path with the common path of just the parent path. Using the commonpath method on just the parent path will regularise the path name in the same way as the comparison that deals with both paths, removing any trailing path separator
    return os.path.commonpath([parent_path]) == os.path.commonpath([parent_path, child_path])

Une doublure précise

Vous pouvez combiner le tout en une instruction if d'une ligne dans Python 3.5. C'est moche, cela inclut des appels inutiles en double à os.path.abspath et il ne rentrera certainement pas dans les directives de longueur de ligne de 79 caractères du PEP 8, mais si vous aimez ce genre de chose, voici:

if os.path.commonpath([os.path.abspath(parent_path_to_test)]) == os.path.commonpath([os.path.abspath(parent_path_to_test), os.path.abspath(child_path_to_test)]):
    # Yes, the child path is under the parent path
17
Tom Bull
def is_subdir(path, directory):
    path = os.path.realpath(path)
    directory = os.path.realpath(directory)
    relative = os.path.relpath(path, directory)
    return not relative.startswith(os.pardir + os.sep)
12
jgoeders

donc, j'en avais besoin, et en raison des critiques à propos de commonprefx, je suis allé d'une manière différente:

def os_path_split_asunder(path, debug=False):
    """
    http://stackoverflow.com/a/4580931/171094
    """
    parts = []
    while True:
        newpath, tail = os.path.split(path)
        if debug: print repr(path), (newpath, tail)
        if newpath == path:
            assert not tail
            if path: parts.append(path)
            break
        parts.append(tail)
        path = newpath
    parts.reverse()
    return parts


def is_subdirectory(potential_subdirectory, expected_parent_directory):
    """
    Is the first argument a sub-directory of the second argument?

    :param potential_subdirectory:
    :param expected_parent_directory:
    :return: True if the potential_subdirectory is a child of the expected parent directory

    >>> is_subdirectory('/var/test2', '/var/test')
    False
    >>> is_subdirectory('/var/test', '/var/test2')
    False
    >>> is_subdirectory('var/test2', 'var/test')
    False
    >>> is_subdirectory('var/test', 'var/test2')
    False
    >>> is_subdirectory('/var/test/sub', '/var/test')
    True
    >>> is_subdirectory('/var/test', '/var/test/sub')
    False
    >>> is_subdirectory('var/test/sub', 'var/test')
    True
    >>> is_subdirectory('var/test', 'var/test')
    True
    >>> is_subdirectory('var/test', 'var/test/fake_sub/..')
    True
    >>> is_subdirectory('var/test/sub/sub2/sub3/../..', 'var/test')
    True
    >>> is_subdirectory('var/test/sub', 'var/test/fake_sub/..')
    True
    >>> is_subdirectory('var/test', 'var/test/sub')
    False
    """

    def _get_normalized_parts(path):
        return os_path_split_asunder(os.path.realpath(os.path.abspath(os.path.normpath(path))))

    # make absolute and handle symbolic links, split into components
    sub_parts = _get_normalized_parts(potential_subdirectory)
    parent_parts = _get_normalized_parts(expected_parent_directory)

    if len(parent_parts) > len(sub_parts):
        # a parent directory never has more path segments than its child
        return False

    # we expect the Zip to end with the short path, which we know to be the parent
    return all(part1==part2 for part1, part2 in Zip(sub_parts, parent_parts))
6
Rob Dennis
def is_in_directory(filepath, directory):
    return os.path.realpath(filepath).startswith(
        os.path.realpath(directory) + os.sep)
5
Juan A. Navarro

J'aime le "chemin dans other_path.parents" abordé mentionné dans une autre réponse parce que je suis un grand fan de pathlib, MAIS je pense que cette approche est un peu lourde (elle crée une instance de chemin pour chaque parent à la racine du chemin). Également le cas où path == other_path échouera avec cette approche, alors que os.commonpath réussirait dans ce cas.

Ce qui suit est une approche différente, avec son propre ensemble d'avantages et d'inconvénients par rapport à d'autres méthodes identifiées dans les différentes réponses:

try:
   other_path.relative_to(path)
except ValueError:
   ...no common path...
else:
   ...common path...

ce qui est un peu plus détaillé mais peut facilement être ajouté en tant que fonction dans le module des utilitaires communs de votre application ou même ajouter la méthode à Path au démarrage.

1
Oliver

J'ai utilisé la fonction ci-dessous pour un problème similaire:

def is_subdir(p1, p2):
    """returns true if p1 is p2 or its subdirectory"""
    p1, p2 = os.path.realpath(p1), os.path.realpath(p2)
    return p1 == p2 or p1.startswith(p2+os.sep)

Après avoir rencontré des problèmes avec le lien symbolique, j'ai modifié la fonction. Maintenant, il vérifie si les deux chemins sont des répertoires.

def is_subdir(p1, p2):
    """check if p1 is p2 or its subdirectory
    :param str p1: subdirectory candidate
    :param str p2: parent directory
    :returns True if p1,p2 are directories and p1 is p2 or its subdirectory"""
    if os.path.isdir(p1) and os.path.isdir(p2):
        p1, p2 = os.path.realpath(p1), os.path.realpath(p2)
        return p1 == p2 or p1.startswith(p2+os.sep)
    else:
        return False
0
Jacek Błocki