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

发布于 2024-11-26 02:14:32 字数 1030 浏览 1 评论 0原文

来自 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.gif 发送请求http://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

Is the following code snippet from a Python WSGI app safe from directory traversal? It reads a file name passed as parameter and returns the named file.

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

I mounted the app under http://localhost:8000/file/{file} and sent requests with the URLs http://localhost:8000/file/../alarm.gif and http://localhost:8000/file/%2e%2e%2falarm.gif. But none of my attempts delivered the (existing) file. So is my code already safe from directory traversal?

New approach

It seems like the following code prevents directory traversal:

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

我不是你的备胎 2024-12-03 02:14:32

您的代码不会阻止目录遍历。您可以使用 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

Your code does not prevent directory traversal. You can guard against this with the os.path module.

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

startdir is now an absolute path where you don't want to allow the path to go outside of. Now let's say we get a filename from the user and they give us the malicious /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'

We have now converted their path into an absolute path relative to our starting path. Since this wasn't in the starting path, it doesn't have the prefix of our starting path.

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

You can check for this in your code. If the commonprefix function returns a path that doesn't start with startdir, then the path is invalid and you should not return the contents.


The above can be wrapped to a static method like so:

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
七秒鱼° 2024-12-03 02:14:32

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

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'

Use only the base name of the user inputed file:

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 strips ../ from the path:

>>> os.path.basename('../../filename')
'filename'
能怎样 2024-12-03 02:14:32

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

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

relpath 负责为我们规范化路径。如果相对路径以..开头,那么你就不允许它。

There's a much simpler solution here:

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

relpath takes care of normalising path for us. And if the relative path starts with .., then you don't allow it.

╰ゝ天使的微笑 2024-12-03 02:14:32

您可以使用 secure_filename 清理文件名。这将删除任何不需要的字符。

from werkzeug.utils import secure_filename

filename = "file.txt../../../../etc/passwd"
safe_filename = secure_filename(filename)
print(safe_filename) 

Output: file.txt

You can sanitize the filename using secure_filename. This will remove any unwanted characters.

from werkzeug.utils import secure_filename

filename = "file.txt../../../../etc/passwd"
safe_filename = secure_filename(filename)
print(safe_filename) 

Output: file.txt
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文