我的代码是否防止目录遍历?

18

以下代码片段来自Python WSGI应用程序,是否安全免受目录遍历攻击?它读取传递的文件名并返回命名的文件。

file_name = request.path_params["file"]
file = open(file_name, "rb")
mime_type = mimetypes.guess_type(file_name)[0]
start_response(status.OK, [('Content-Type', mime_type)])
return file
我将应用程序挂载在 http://localhost:8000/file/{file} 下,并使用以下 URL 发送请求:http://localhost:8000/file/../alarm.gifhttp://localhost:8000/file/%2e%2e%2falarm.gif,但是我的所有尝试都未能获取到(现有的)文件。那么,我的代码是否已经安全,不受目录遍历攻击的影响了呢? 新方法 看起来,以下代码可以防止目录遍历攻击:
file_name = request.path_params["file"]
absolute_path = os.path.join(self.base_directory, file_name)
normalized_path = os.path.normpath(absolute_path)

# security check to prevent directory traversal
if not normalized_path.startswith(self.base_directory):
    raise IOError()

file = open(normalized_path, "rb")
mime_type = mimetypes.guess_type(normalized_path)[0]
start_response(status.OK, [('Content-Type', mime_type)])
return file

1
如果您提供了绝对路径,会发生什么? - Katriel
好主意!你的建议让我找到了答案:它是不安全的!目录遍历被使用框架的另一部分“意外地”阻止了。 - deamon
3个回答

19

你的代码没有防止目录遍历攻击。你可以使用os.path模块来防范。

>>> import os.path
>>> os.curdir
'.'
>>> startdir = os.path.abspath(os.curdir)
>>> startdir
'/home/jterrace'

startdir现在是一个绝对路径,您不希望允许路径超出该范围。现在让我们假设用户给了我们恶意的文件名/etc/passwd

>>> filename = "/etc/passwd"
>>> requested_path = os.path.relpath(filename, startdir)
>>> requested_path
'../../etc/passwd'
>>> requested_path = os.path.abspath(requested_path)
>>> requested_path
'/etc/passwd'

我们现在将它们的路径转换为相对于我们起始路径的绝对路径。由于这不在起始路径中,因此它没有我们起始路径的前缀。

>>> os.path.commonprefix([requested_path, startdir])
'/'

您可以在代码中检查这一点。如果commonprefix函数返回的路径不以startdir开头,则该路径无效,您不应该返回其内容。


可以将上述内容包装成静态方法,具体如下:

import os 

def is_directory_traversal(file_name):
    current_directory = os.path.abspath(os.curdir)
    requested_path = os.path.relpath(file_name, start=current_directory)
    requested_path = os.path.abspath(requested_path)
    common_prefix = os.path.commonprefix([requested_path, current_directory])
    return common_prefix != current_directory

2
不要依赖于相对于当前工作目录的操作,因为在 Web 应用程序中它可以是任何东西。始终应该以绝对路径为起点,无论是硬编码还是从 __file__ 计算得出。 - Graham Dumpleton
@graham-dumpleton 这与相对路径或绝对路径无关。一个路径总是可能会失效,除非你进行此类的健全性检查。 - jterrace
你似乎没有完全理解我的意思。你在例子中提供了“startdir = os.path.abspath(os.curdir)”这一行代码。这将把“startdir”设置为当前工作目录。在Python Web应用程序中,无法保证当前工作目录是什么。因此,这是一个不好的例子,因为人们会盲目地复制和粘贴代码,而不理解他们应该将其锚定在具有对他们的应用程序有意义的绝对路径上,而不是依赖于os.getcwd()在调用os.path.abspath(os.curdir)时返回什么。 - Graham Dumpleton
你可以使用类似于dotdotpwn(https://github.com/wireghoul/dotdotpwn)的目录遍历攻击模糊器来测试此代码片段,例如:'./dotdotpwn.pl -m http-url -u "http://google.com/?q=TRAVERSAL" -O -k "root:"'。 - evandrix
1
注意,此代码依赖于 startdir 以正斜杠结尾,但在这种情况下并不是这样!此代码将错误地接受输入 ../jterrace-attack/foobar - Flimm

4

仅使用用户输入文件的基本名称:

file_name = request.path_params["file"]
file_name = os.path.basename(file_name)
file = open(os.path.join("/path", file_name), "rb")

os.path.basename 函数从路径中剥离了 ../

>>> os.path.basename('../../filename')
'filename'

1
这并不能防止目录遍历,因为 file_name 可能包含 ../!但是你的代码还是有帮助的。 - deamon
1
抱歉,您的解决方案似乎也可以工作。但它只适用于单个目录级别(没有子目录)。如果您稍微修改答案以便我可以再次投票,我会更正我的反对意见。 - deamon
@daemon os.path.basename 函数会从路径中剥离掉 ../。请查看更新的答案。 - Clodoaldo Neto
2
os.path.basename 会去掉 ../,但它也会去掉所有路径相关的信息。os.path.basename("a/b/c") 返回 "c" - Flimm
2
@Flimm:是的,这是有意为之的。 - Clodoaldo Neto
我喜欢这个方法,因为它简单易懂,并且在你想要在一个固定的目录(没有子目录)中工作并只从用户那里接收文件名时做正确的事情。 - Augusto Destrero

2

这里有一个更简单的解决方案:

relative_path = os.path.relpath(path, start=self.test_directory)
has_dir_traversal = relative_path.startswith(os.pardir)

relpath 负责对路径进行规范化。如果相对路径以 .. 开头,则不允许使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接