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

18 投票
3 回答
15101 浏览
提问于 2025-04-16 22:09

下面这段代码是一个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} 这个地址下,然后尝试用这两个网址发送请求: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

3 个回答

2

这里有一个更简单的解决办法:

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

relpath 可以帮我们处理路径的规范化。如果相对路径是以 .. 开头的,那就不允许使用。

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

撰写回答