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

我喜欢用 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

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

UPDATE - Solution 如果中间目录是符号链接,则上述代码不起作用。 为了防止这种情况,必须使用 realpath而不是 abspath

更新: 添加一个尾随/to 目录,以解决 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
55823 次浏览

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

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

为了得到更好的结果,我会对照文件名测试 common 前缀的结果,如下所示:

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

因此,我需要这样做,由于对通用前缀的批评,我走了一条不同的道路:

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

基于这里的另一个答案,带有更正,并使用一个用户友好的名称:

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

Python3的 pathlib模块通过它的 父母属性使这一点变得简单明了:

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

许多建议的方法存在问题

如果您要使用字符串比较或 os.path.commonprefix方法来测试目录亲缘关系,那么使用类似命名的路径或相对路径时,这些方法很容易出错。例如:

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

Rob Dennis 的 上一个答案提供了一个很好的方法来比较路径亲子关系,而不会遇到这些问题。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])

准确的一行

You can combine the whole lot into a one-line if statement in Python 3.5. It's ugly, it includes unnecessary duplicate calls to os.path.abspath and it definitely won't fit in the PEP 8 79-character line-length guidelines, but if you like that kind of thing, here goes:

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

Python 3.9的新特性

pathlibPurePath上有一个名为 is_relative_to的新方法,它直接执行这个函数。如果您需要了解如何使用它,您可以阅读 the python documentation on how is_relative_to works。或者您可以查看 my other answer以获得关于如何使用它的更完整的描述。

我喜欢“其他路径中的路径”。在另一个答案中提到的“父母”方法,因为我是 pathlib 的大粉丝,但是我觉得这种方法有点沉重(它为每个父母创建一个 Path 实例到 Path 的根)。此外,在这种情况下,path = = other _ path 将失败,而 os.commpath 将在这种情况下成功。

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

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

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

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

对于类似的问题,我使用了以下函数:

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

在您的启发下,这个方法被添加到我的工具中:

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

Python 3.9的新特性

pathlibPurePath上有一个名为 is_relative_to的新方法,它直接执行这个函数。你可以阅读 关于 is_relative_to如何工作的 python 文档,或者使用这个例子:

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