如何检查目录是否是另一个目录的子目录

时间:2010-09-28 12:50:23

标签: python security validation filesystems

我喜欢在Python中编写一个模板系统,它允许包含文件。

e.g。

    This is a template
    You can safely include files with safe_include`othertemplate.rst`

如您所知,包含文件可能很危险。例如,如果我在允许用户创建自己的模板的Web应用程序中使用模板系统,他们可能会执行类似

的操作。
I want your passwords: safe_include`/etc/password`

因此,我必须限制将文件包含在文件中,例如在某个子目录中(例如/home/user/templates

现在的问题是:我如何检查/home/user/templates/includes/inc1.rst是否在/home/user/templates的子目录中?

以下代码是否有效且安全?

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

只要allow_symlink为假,我认为它应该是安全的。如果用户能够创建这样的链接,那么允许符号链接会使其不安全。

更新 - 解决方案 如果中间目录是符号链接,则上述代码不起作用。 为防止出现这种情况,您必须使用realpath代替abspath

UPDATE:添加一个尾随/目录以解决commonprefix()问题,Reorx指出。

由于符号链接扩展到其真实目的地

,这也使allow_symlink变得不必要
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

12 个答案:

答案 0 :(得分:28)

Python 3' pathlib模块使用Path.parents属性使其变得简单明了。例如:

from pathlib import Path

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

然后:

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

答案 1 :(得分:13)

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)

答案 2 :(得分:11)

许多建议方法的问题

如果您要使用字符串比较或os.path.commonprefix方法测试目录父级,则这些方法容易出现类似命名路径或相对路径的错误。例如:

  • /path/to/files/myfile将使用许多方法显示为/path/to/file的子路径。
  • 许多方法都不会将
  • /path/to/files/../../myfiles显示为/path/myfiles/myfile的父级。事实上,它是。

Rob Dennis的previous answer提供了一种比较路径父母而不会遇到这些问题的好方法。 Python 3.4添加了pathlib模块,它可以以更复杂的方式执行这些路径操作,可选择不引用底层操作系统。 jme在another previous answer中描述了如何使用pathlib来准确确定一条路径是否是另一条路径的子路径。如果您不想使用pathlib(不确定原因,它非常棒),那么Python 3.5在os.path中引入了一个基于操作系统的新方法,允许您执行路径父子使用更少的代码以类似的准确和无差错的方式进行检查。

Python 3.5的新功能

Python 3.5引入了函数os.path.commonpath。这是一种特定于运行代码的操作系统的方法。您可以通过以下方式使用commonpath来准确确定路径父级:

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])

准确的单行

您可以将整个批次合并到Python 3.5中的单行if语句中。这很丑陋,它包括对os.path.abspath的不必要的重复调用,它绝对不符合PEP 8 79字符行长指南,但是如果你喜欢这样的话,那就去了:

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

答案 3 :(得分:10)

os.path.realpath(path):返回指定文件名的规范路径,消除路径中遇到的任何符号链接(如果操作系统支持它们)。

在目录和子目录名称上使用它,然后检查后者以前者开始。

答案 4 :(得分:6)

所以,我需要这个,并且由于对commonprefx的批评,我采取了不同的方式:

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))

答案 5 :(得分:4)

def is_in_directory(filepath, directory):
    return os.path.realpath(filepath).startswith(
        os.path.realpath(directory) + os.sep)

答案 6 :(得分:3)

Python 3.9 的新功能

pathlibPurePath 上有一个名为 is_relative_to 的新方法,它直接执行此功能。您可以阅读the python documentation on how is_relative_to works,或使用以下示例:

from pathlib import Path

child_path = Path("/path/to/file")
if child_path.is_relative_to("/path"):
    print("/path/to/file is a child of /path") # This prints
if child_path.is_relative_to("/anotherpath"):
    print("/path/to/file is a child of /anotherpath") # This does not print

答案 7 :(得分:2)

我喜欢在另一个答案中提及的“other_path.parents中的路径”,因为我是pathlib的忠实粉丝,但我觉得这个方法有点重(它为每个父节点创建一个Path实例到路径的根目录)。此外,path == other_path将使用该方法失败,而os.commonpath将在该情况下成功。

以下是一种不同的方法,与各种答案中确定的其他方法相比,它们各有利弊:

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

有点冗长,但可以很容易地在应用程序的常用实用程序模块中添加为函数,甚至可以在启动时将该方法添加到Path。

答案 8 :(得分:0)

我使用下面的函数来解决类似的问题:

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)

遇到符号链接问题后,我修改了该函数。现在,它会检查两个路径是否均为目录。

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

答案 9 :(得分:0)

在您的启发下,此方法已添加到我的实用程序中:

def is_in_basefolder(path_to_check: PosixPath, basefolder: PosixPath):
        """
        check if a given path is in base folder
        
        parameters:
            path_to_check: a path to match with base folder
            basefolder: the base folder
        """
        path = path_to_check.resolve()
        base = basefolder.resolve()
        
        if path == base:
            return True
        
        if base.stem in path.parts:
            return True
        else:
            return False

答案 10 :(得分:-1)

我会测试commonprefix对文件名的结果,以获得更好的答案,如下所示:

def is_in_folder(filename, folder='/tmp/'):
    # normalize both parameters
    fn = os.path.normpath(filename)
    fd = os.path.normpath(folder)

    # get common prefix
    commonprefix = os.path.commonprefix([fn, fd])
    if commonprefix == fd:
        # in case they have common prefix, check more:
        sufix_part = fn.replace(fd, '')
        sufix_part = sufix_part.lstrip('/')
        new_file_name = os.path.join(fd, sufix_part)
        if new_file_name == fn:
            return True
        pass
    # for all other, it's False
    return False

答案 11 :(得分:-1)

基于此处的另一个答案,带有更正,并且用户友好名称:

def isA_subdirOfB_orAisB(A, B):
    """It is assumed that A is a directory."""
    relative = os.path.relpath(os.path.realpath(A), 
                               os.path.realpath(B))
    return not (relative == os.pardir
            or  relative.startswith(os.pardir + os.sep))