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

Sim*_*mon 38 python filesystems security validation

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

例如

    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
Run Code Online (Sandbox Code Playgroud)

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

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

更新:添加一个尾随/目录以解决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
Run Code Online (Sandbox Code Playgroud)

jme*_*jme 36

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

from pathlib import Path

root = Path('/path/to/root')
child = root / 'some' / 'child' / 'dir'
other = Path('/some/other/path')
Run Code Online (Sandbox Code Playgroud)

然后:

>>> root in child.parents
True
>>> other in child.parents
False
Run Code Online (Sandbox Code Playgroud)

  • Python 3.9 将“is_relative_to”添加到“pathlib”中,直接执行此操作。 (7认同)
  • Python 3 的大多数 Pythonic 方式,`pathlib` 确实让事情更容易阅读。值得一提的是,如果您在检查“A in B.parents”之前可能想调用“.resolve()”的相对路径玩了很多。 (4认同)
  • `root in [child] + [p for p in child.parents]` 可以简化为 `root in (child, *child.parents)` (3认同)
  • 值得一提的是,首先对“child”调用“resolve()”是一个硬性要求。否则,如果子级位于根的任何部分内,它将返回“True”。 (3认同)
  • 如果你认为一个路径是它自己的子/父(例如,如果你想测试路径是`/ a/b/c`还是`/ a/b/c`的子目录)那么你可以在[child]中使用`root +在child.parents中使用[p表示p] (2认同)
  • @PhilipCouling我想这取决于你想做什么。在大多数情况下,遵循符号链接被认为是危险的。只有使用实际的文件夹结构才更安全。例如,许多网络服务器漏洞利用都是继承于此。 (2认同)

Tom*_*ull 15

许多建议的方法存在问题

如果您要使用字符串比较或os.path.commonprefix方法测试目录父项,则这些路径或相对路径会出现错误.例如:

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

以前的答案罗布丹尼斯提供了一个很好的方法来比较路径血统,而不会遇到这些问题.Python 3.4添加了pathlib可以以更复杂的方式执行这些路径操作的模块,可选地不引用底层操作系统.jme在另一个先前的答案中描述了如何使用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])
Run Code Online (Sandbox Code Playgroud)

准确的单线

您可以将整个批次合并到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
Run Code Online (Sandbox Code Playgroud)

  • WTF是否有必要采取1条路径的commonpath?imo:abs_parent_path == commonpath([abs_parent_path,abs_child_path])应该足够... (3认同)
  • 注意:“is_relative_to()”方法不能准确处理“../”。例如:`Path('/test1/../test2/myfile.txt').is_relative_to('/test2') == False`。如果您的路径中有这些,则需要使用 **准确的单行代码** 而不是 Python 3.9 的新建议。 (2认同)

jgo*_*ers 12

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)
Run Code Online (Sandbox Code Playgroud)

  • @TorstenBronger好点.因此,答案是错误的,除非最后一行改为`return not(relative == os.pardir或relative.startswith(os.pardir + os.sep))`.顺便说一句,如果我们坚持使用_proper_子目录,那么还要检查`(relative == os.curdir)`. (9认同)
  • `os.path.relpath`在我的测试中不包含`os.sep`,例如`os.path.relpath("/ a","/ a/b")`. (4认同)
  • 如果您不是使用 Python 2,请不要使用它,而是使用 pathlib,如其他示例所示。如果您使用的是 Python 2:请移步。 (2认同)

Tom*_*ull 12

Python 3.9 的新增功能

pathlibPurePath有一个被调用的新方法is_relative_to直接执行此功能。您可以阅读有关如何工作的python 文档is_relative_to,或使用以下示例:

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
Run Code Online (Sandbox Code Playgroud)


bla*_*aze 9

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

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

  • 安全漏洞:例如,请参阅Reorx对OP的评论. (10认同)
  • 实际上,你需要在调用startswith时附加os.sep(如果还没有包含),如jgoeders所述, (3认同)
  • 'a/b/cde' 以 'a/b/c' 开头,但不是子目录。 (2认同)

Rob*_*nis 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))
Run Code Online (Sandbox Code Playgroud)


Jua*_*rro 5

def is_in_directory(filepath, directory):
    return os.path.realpath(filepath).startswith(
        os.path.realpath(directory) + os.sep)
Run Code Online (Sandbox Code Playgroud)