在python中清理文件路径

Sanitizing a file path in python

我有一个文件浏览器应用程序,它将目录及其内容公开给用户。

我想清理用户输入,这是一个文件路径,因此它不允许绝对路径,如'/ tmp /'和相对路径,如'../../etc'

是否有跨平台执行此操作的python函数?


也适用于寻找摆脱"A /./ B" - >"A / B和"A / B /../ C" - >"A / C"路径的方法的人们。
您可以使用os.path.normpath


python的综合文件路径杀菌器

我对任何可用的消毒方法都不满意,所以我写了自己的,相对全面的路径消毒剂。这适用于从公共端点(http上传,REST端点等)获取输入,并确保如果将数据保存在生成??的文件路径中,则不会损坏您的系统**。 (注意:此代码针对Python 3+,您可能需要进行一些更改才能使其在2.x上运行)

*不保证!如果没有彻底检查,请不要依赖此代码。

**再次,没有保证!您仍然可以做一些疯狂的事情,并将* nix系统上的根路径设置为/dev//bin/或类似的东西。不要那样做。在Windows上也有一些可能导致损坏的边缘情况(例如设备文件名),你可以从werkzeugutils检查secure_filename方法,以便在处理这些问题时有一个好的开始,如果你是定位Windows。

这个怎么运作

  • 您需要指定根路径,消毒者将确保返回的所有路径都在此根目录下。检查get_root_path功能以查找执行此操作的位置。确保根路径的值来自您自己的配置,而不是来自用户的输入!
  • 有一个文件名sanitiser,其中:

    • 将unicode转换为ASCII
    • 将路径分隔符转换为下划线
    • 仅允许文件名中白名单中的某些字符。白名单包括所有小写和大写字母,所有数字,连字符,下划线,空格,开始和结束圆括号以及句号(句号)。您可以根据需要自定义此白名单。
    • 确保所有名称至少有一个字母或数字(以避免名称如'..')
  • 要获得有效的文件路径,您应该调用make_valid_file_path。您可以选择在path参数中将其传递给子目录路径。这是根路径下面的路径,可以来自用户输入。您可以选择在filename参数中传递文件名,这也可以来自用户输入。您传递的文件名中的任何路径信息都不会用于确定文件的路径,而是将其展平为文件名的有效安全组件。

    • 如果没有路径或文件名,它将返回正确格式化为主机文件系统的根路径,并带有尾随路径分隔符(/)。
    • 如果存在子目录路径,则会将其拆分为其组成部分,使用文件名sanitiser对每个部分进行清理,并在没有前导路径分隔符的情况下重建路径。
    • 如果有文件名,它将使用杀菌剂清理名称。
    • 它将os.path.join路径组件以获取文件的最终路径。
    • 最后仔细检查生成的路径是否有效且安全,它会检查生成的路径是否位于根路径下的某个位置。通过拆分和比较路径的组成部分来正确地完成此检查,而不是仅仅确保一个字符串从另一个字符串开始。

好的,足够的警告和描述,这是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import os

def ensure_directory_exists(path_directory):
    if not os.path.exists(path_directory):
        os.makedirs(path_directory)

def os_path_separators():
    seps = []
    for sep in os.path.sep, os.path.altsep:
        if sep:
            seps.append(sep)
    return seps

def sanitise_filesystem_name(potential_file_path_name):
    # Sort out unicode characters
    valid_filename = normalize('NFKD', potential_file_path_name).encode('ascii', 'ignore').decode('ascii')
    # Replace path separators with underscores
    for sep in os_path_separators():
        valid_filename = valid_filename.replace(sep, '_')
    # Ensure only valid characters
    valid_chars ="-_.() {0}{1}".format(string.ascii_letters, string.digits)
    valid_filename ="".join(ch for ch in valid_filename if ch in valid_chars)
    # Ensure at least one letter or number to ignore names such as '..'
    valid_chars ="{0}{1}".format(string.ascii_letters, string.digits)
    test_filename ="".join(ch for ch in potential_file_path_name if ch in valid_chars)
    if len(test_filename) == 0:
        # Replace empty file name or file path part with the following
        valid_filename ="(Empty Name)"
    return valid_filename

def get_root_path():
    # Replace with your own root file path, e.g. '/place/to/save/files/'
    filepath = get_file_root_from_config()
    filepath = os.path.abspath(filepath)
    # ensure trailing path separator (/)
    if not any(filepath[-1] == sep for sep in os_path_separators()):
        filepath = '{0}{1}'.format(filepath, os.path.sep)
    ensure_directory_exists(filepath)
    return filepath

def path_split_into_list(path):
    # Gets all parts of the path as a list, excluding path separators
    parts = []
    while True:
        newpath, tail = os.path.split(path)
        if newpath == path:
            assert not tail
            if path and path not in os_path_separators():
                parts.append(path)
            break
        if tail and tail not in os_path_separators():
            parts.append(tail)
        path = newpath
    parts.reverse()
    return parts

def sanitise_filesystem_path(potential_file_path):
    # Splits up a path and sanitises the name of each part separately
    path_parts_list = path_split_into_list(potential_file_path)
    sanitised_path = ''
    for path_component in path_parts_list:
        sanitised_path = '{0}{1}{2}'.format(sanitised_path, sanitise_filesystem_name(path_component), os.path.sep)
    return sanitised_path

def check_if_path_is_under(parent_path, child_path):
    # Using the function to split paths into lists of component parts, check that one path is underneath another
    child_parts = path_split_into_list(child_path)
    parent_parts = path_split_into_list(parent_path)
    if len(parent_parts) > len(child_parts):
        return False
    return all(part1==part2 for part1, part2 in zip(child_parts, parent_parts))

def make_valid_file_path(path=None, filename=None):
    root_path = get_root_path()
    if path:
        sanitised_path = sanitise_filesystem_path(path)
        if filename:
            sanitised_filename = sanitise_filesystem_name(filename)
            complete_path = os.path.join(root_path, sanitised_path, sanitised_filename)
        else:
            complete_path = os.path.join(root_path, sanitised_path)
    else:
        if filename:
            sanitised_filename = sanitise_filesystem_name(filename)
            complete_path = os.path.join(root_path, sanitised_filename)
        else:
            complete_path = complete_path
    complete_path = os.path.abspath(complete_path)
    if check_if_path_is_under(root_path, complete_path):
        return complete_path
    else:
        return None


这将阻止用户输入../../../../etc/shadow之类的文件名,但也不允许basedir以下子目录中的文件:

1
2
3
4
from pathlib import Path
test_path = (Path(basedir) / user_input).resolve()
if test_path.parent != Path(basedir).resolve():
    raise Exception(f"Filename {test_path} is not in {Path(basedir)} directory")

如果要允许basedir以下的子目录:

1
2
if not Path(basedir).resolve() in test_path.resolve().parents:
    raise Exception(f"Filename {test_path} is not in {Path(basedir)} directory")

也许不仅仅是清理路径(黑名单),您只能允许(白名单)有效路径。

python在os.path中提供了各种工具,以独立于os的方式执行此操作