按条件加载模块 Python

2 投票
3 回答
1275 浏览
提问于 2025-04-17 20:15

我写了一个主要的Python模块,它需要加载一个文件解析器才能工作。最开始我只有一个文本解析器模块,但现在我需要为不同的情况添加更多的解析器。
parser_class1.py
parser_class2.py
parser_class3.py

每次运行时只需要其中一个解析器,所以我在想通过命令行来加载它:

mmain.py -p parser_class1

为此,我写了这段代码,以便在调用主模块时选择要加载的解析器:

#!/usr/bin/env python

import argparse
aparser = argparse.ArgumentParser()
aparser.add_argument('-p',
            action='store',
            dest='module',
            help='-p module to import')
results = aparser.parse_args()

if not results.module:
    aparser.error('Error! no module')
try:
    exec("import %s" %(results.module))
    print '%s imported done!'%(results.module)
except ImportError, e:
    print e

但是,我在阅读时发现这种方式可能不太安全,也许不是标准做法……

那么,这种方法可以吗?还是我应该找其他方式来实现?为什么?谢谢,欢迎任何评论。

3 个回答

0

你应该考虑所有可能需要导入的模块,以便进行解析功能,然后使用一个选择语句或者字典来加载正确的模块。例如:

import parser_class1, parser_class2, parser_class3

parser_map = {
    'class1': parser_class1,
    'class2': parser_class2,
    'class3': parser_class3,
}

if not args.module:
    #report error
    parser = None
else:
    parser = parser_map[args.module]
#perform work with parser

如果在这个例子中加载任何一个 parser_classN 模块的成本很高,你可以定义一些简单的函数(比如 def get_class1(): import parser_class1; return parser_class1),然后把那一行改成 parser = parser_map[args.module]()

使用 exec 这个选项可能非常危险,因为你是在执行未经验证的用户输入。想象一下,如果你的用户做了这样的事情 -

mmain.py -p "parser_class1; some_function_or_code_that_is_malicious()"
5

其实你可以在一个条件语句里执行import这个命令:

if x:
    import module1a as module1
else:
    import module1b as module1

这样做可以让你以不同的方式处理各种允许的模块导入,基本上就是提前设置好要导入的模块,然后用一种类似于跳转的方式来导入合适的模块……如果你想让用户随便导入任何模块,那么可以使用__import__这个函数,而不是eval

更新:

正如@thedox在评论中提到的,as module1这一部分是加载不同底层代码但功能相似的API的常用方式。如果你打算用完全不同的API做完全不同的事情,那就不应该这样做。在这种情况下,更合理的做法是把与特定导入相关的代码放在那个导入语句旁边:

if ...:
    import module1
    # do some stuff with module1 ...

else:
    import module2
    # do some stuff with module2 ...

至于安全性,如果你允许用户导入一些任意的代码集(比如他们自己的模块),这和对用户输入使用eval其实没什么区别。基本上都是同样的漏洞:用户可以让你的程序执行他们自己的代码。

我认为没有一种真正安全的方法可以让用户导入任意模块。唯一的例外是如果他们没有访问文件系统的权限,因此无法创建新的代码被导入。在这种情况下,你基本上又回到了允许列表的情况,最好还是实现一个明确的允许列表,以防将来用户获得文件系统访问权限时出现新的漏洞。

1

下面是如何使用 __import__() 的介绍。

allowed_modules = ['os', 're', 'your_module', 'parser_class1.py', 'parser_class2.py']

if not results.module:
    aparser.error('Error! no module')
try:
    if results.module in allowed_modules:
        module = __import__(results.module)
        print '%s imported as "module"'%(results.module)
    else:
        print 'hey what are you trying to do?'
except ImportError, e:
    print e

module.your_function(your_data)

EVAL__IMPORT__() 的区别

使用 eval 允许用户在你的电脑上运行任何代码。这是非常危险的,千万不要这样做。而 __import__() 只允许用户加载模块,表面上看起来不允许用户随意运行代码。但实际上,它的安全性也只是表面上的。

如果没有 allowed_modules 的保护,提议的函数仍然存在风险,因为它可能允许加载一个任意的模块,而这个模块可能包含一些恶意代码。一旦加载,攻击者可能会在某个地方(比如共享文件夹、FTP 文件夹、你的网站服务器管理的上传文件夹等)放置一个文件,然后通过你的参数调用它。

白名单

使用 allowed_modules 可以减轻这个问题,但并不能完全解决:为了进一步增强安全性,你还需要检查攻击者是否在你的脚本文件夹中写了 "os.py"、"re.py"、"your_module.py"、"parser_class1.py" 等文件,因为 Python 首先会在这里搜索模块(文档)。

最终,你可以将 parser_class*.py 的代码与一个哈希列表进行比较,就像 sha1sum 所做的那样。

最后的提醒:如果用户对你的脚本文件夹有写入权限,那么你无法确保代码绝对安全。

撰写回答