Python 代码注入与动态执行漏洞:exec()与eval()的安全替代方案
大家好,今天我们要深入探讨 Python 中两个强大但危险的内置函数:exec() 和 eval()。它们允许我们动态执行 Python 代码,这在某些场景下非常有用,但如果使用不当,会带来严重的安全风险,导致代码注入漏洞。本次讲座将详细分析这些风险,并介绍更安全的替代方案。
exec() 和 eval():动态执行的利器与风险
1. exec() 函数
exec() 函数用于执行存储在字符串或代码对象中的 Python 代码。它的基本语法如下:
exec(object, globals=None, locals=None)
object: 必需参数,可以是包含 Python 代码的字符串、字节码对象或者代码对象。globals: 可选参数,一个字典,表示全局命名空间。如果省略,则使用当前的全局命名空间。locals: 可选参数,一个字典,表示局部命名空间。如果省略,则使用当前的局部命名空间。
示例:
code_string = "x = 5; y = 10; print(x + y)"
exec(code_string) # 输出 15
# 使用 globals 和 locals
global_vars = {'a': 1}
local_vars = {'b': 2}
exec("print(a + b)", global_vars, local_vars) # 输出 报错,因为global中没有b,local中没有a
exec() 的强大之处在于它可以执行任何复杂的 Python 代码,包括函数定义、类定义、模块导入等。
2. eval() 函数
eval() 函数用于计算一个字符串表达式的值。它的基本语法如下:
eval(expression, globals=None, locals=None)
expression: 必需参数,一个字符串,表示一个 Python 表达式。globals: 可选参数,一个字典,表示全局命名空间。如果省略,则使用当前的全局命名空间。locals: 可选参数,一个字典,表示局部命名空间。如果省略,则使用当前的局部命名空间。
示例:
result = eval("2 + 3 * 4") # result 的值为 14
print(result)
# 使用 globals 和 locals
x = 10
result = eval("x + 5") # 输出 15
global_vars = {'x': 20}
result = eval("x + 5", global_vars) # 输出 25
eval() 专注于计算表达式,并返回计算结果。它不允许执行语句,例如赋值语句或控制流语句。
3. 安全风险:代码注入
exec() 和 eval() 的主要安全风险在于它们可以执行任意代码,这意味着如果用户能够控制传递给这些函数的字符串,他们就可以注入恶意代码。
代码注入示例(exec()):
def execute_code(user_input):
exec(user_input)
user_input = input("请输入 Python 代码:")
execute_code(user_input)
如果用户输入 os.system('rm -rf /') (在 Linux/macOS 系统上) 或 os.system('del /f /s /q C:\*') (在 Windows 系统上),这段代码将会被执行,导致系统数据丢失。
代码注入示例(eval()):
def calculate(expression):
result = eval(expression)
return result
user_input = input("请输入一个表达式:")
result = calculate(user_input)
print("结果:", result)
如果用户输入 __import__('os').system('rm -rf /'),同样会导致严重的系统安全问题。 eval 也能执行语句,通过 __import__ 导入os模块再执行危险命令。
安全替代方案:限制与沙箱
为了避免 exec() 和 eval() 带来的安全风险,我们需要寻找更安全的替代方案。以下是一些常用的策略:
1. 避免使用 exec() 和 eval()
最简单也是最有效的方法是尽量避免使用 exec() 和 eval()。重新设计应用程序的架构,寻找其他不需要动态执行代码的方法。很多时候,看似需要动态执行的场景,实际上可以通过更安全的方式实现。
2. 使用 ast.literal_eval()
如果需要安全地计算字面量表达式(例如数字、字符串、布尔值、列表、字典等),可以使用 ast.literal_eval() 函数。这个函数只会计算字面量表达式,不会执行任何其他的 Python 代码,因此更加安全。
import ast
def safe_calculate(expression):
try:
result = ast.literal_eval(expression)
return result
except (ValueError, SyntaxError):
return "Invalid expression"
user_input = input("请输入一个字面量表达式:")
result = safe_calculate(user_input)
print("结果:", result)
如果用户输入 2 + 3 * 4,ast.literal_eval() 会抛出 ValueError 异常,因为这并不是一个字面量表达式。但是,如果用户输入 [1, 2, 3],ast.literal_eval() 会安全地计算出结果 [1, 2, 3]。
3. 使用 json.loads() 或 pickle.loads()
如果需要反序列化 JSON 或 Pickle 格式的数据,可以使用 json.loads() 或 pickle.loads() 函数。但是,pickle.loads() 本身也存在安全风险,因为它可以反序列化任意 Python 对象。因此,除非你完全信任数据的来源,否则不要使用 pickle.loads()。使用 json.loads() 相对安全,因为它只能反序列化 JSON 数据。
4. 白名单验证
如果必须使用 exec() 或 eval(),可以对用户输入进行白名单验证,只允许执行特定的代码。例如,可以创建一个允许执行的函数列表,然后只允许用户调用这些函数。
import re
allowed_functions = ['abs', 'round', 'max', 'min']
def safe_execute(user_input):
# 简单的白名单验证
if not re.match(r"^[a-zA-Z0-9()+-*/s,]+$", user_input):
return "Invalid input"
# 检查是否调用了不允许的函数
for func in allowed_functions:
if func + '(' in user_input:
break
else:
return "不允许使用该函数"
try:
result = eval(user_input, {'__builtins__': None}, {func: __builtins__.__dict__[func] for func in allowed_functions})
return result
except Exception as e:
return str(e)
user_input = input("请输入一个表达式 (只允许使用 abs, round, max, min):")
result = safe_execute(user_input)
print("结果:", result)
这段代码首先使用正则表达式验证用户输入是否只包含字母、数字、括号、加减乘除、空格和逗号。然后,它检查用户输入是否调用了允许的函数。如果一切正常,它会使用 eval() 函数计算表达式的值,并将 __builtins__ 设置为 None,以防止用户访问内置函数,只允许访问白名单中的函数。
5. 使用沙箱环境
沙箱环境是一种隔离的运行环境,可以限制代码的访问权限。可以使用第三方库(例如 RestrictedPython)来创建沙箱环境,并在沙箱中执行不受信任的代码。
from RestrictedPython import compile_restricted
from RestrictedPython import safe_builtins
from RestrictedPython import limited_builtins
def execute_in_sandbox(code):
try:
byte_code = compile_restricted(code, '<string>', 'exec')
loc = {}
exec(byte_code, {'__builtins__': safe_builtins}, loc)
return loc.get('result')
except Exception as e:
return str(e)
user_code = input("输入要在沙箱中执行的代码:")
result = execute_in_sandbox(user_code)
print("沙箱执行结果:", result)
这个例子使用了 RestrictedPython 库来创建一个受限的执行环境。compile_restricted 函数编译代码,使其只能访问安全的操作。safe_builtins 提供了一组安全的内置函数。然后,代码在受限的环境中执行,任何尝试进行不安全操作都会被阻止。
6. 使用安全策略
一些编程语言提供了安全策略,可以限制代码的访问权限。例如,Java 提供了安全管理器,可以控制代码可以访问的资源。虽然 Python 本身没有内置的安全策略,但可以使用第三方库来实现类似的功能。
代码示例与对比
为了更好地理解这些替代方案,我们来看一些具体的代码示例,并比较它们的安全性。
| 方案 | 代码示例 | 安全性 | 适用场景 |
|---|---|---|---|
ast.literal_eval() |
python import ast def safe_calculate(expression): try: result = ast.literal_eval(expression) return result except (ValueError, SyntaxError): return "Invalid expression" user_input = input("请输入一个字面量表达式:") result = safe_calculate(user_input) print("结果:", result) |
非常高 | 安全地计算字面量表达式,例如数字、字符串、布尔值、列表、字典等。 |
| 白名单验证 | python import re allowed_functions = ['abs', 'round', 'max', 'min'] def safe_execute(user_input): if not re.match(r"^[a-zA-Z0-9()+-*/s,]+$", user_input): return "Invalid input" for func in allowed_functions: if func + '(' in user_input: break else: return "不允许使用该函数" try: result = eval(user_input, {'__builtins__': None}, {func: __builtins__.__dict__[func] for func in allowed_functions}) return result except Exception as e: return str(e) user_input = input("请输入一个表达式 (只允许使用 abs, round, max, min):") result = safe_execute(user_input) print("结果:", result) |
较高,但需谨慎 | 当需要动态执行代码,但可以预先确定允许执行的代码范围时。 |
| 沙箱环境 | python from RestrictedPython import compile_restricted from RestrictedPython import safe_builtins def execute_in_sandbox(code): try: byte_code = compile_restricted(code, '<string>', 'exec') loc = {} exec(byte_code, {'__builtins__': safe_builtins}, loc) return loc.get('result') except Exception as e: return str(e) user_code = input("输入要在沙箱中执行的代码:") result = execute_in_sandbox(user_code) print("沙箱执行结果:", result) |
较高,取决于沙箱配置 | 当需要执行不受信任的代码,但需要限制其访问权限时。 |
最佳实践
以下是一些使用 exec() 和 eval() 的最佳实践:
- 尽量避免使用
exec()和eval()。 重新设计应用程序的架构,寻找其他不需要动态执行代码的方法。 - 如果必须使用
exec()或eval(),请对用户输入进行严格的验证。 只允许执行特定的代码,并限制代码的访问权限。 - 使用沙箱环境。 在沙箱中执行不受信任的代码,以限制其访问权限。
- 定期审查代码。 检查代码中是否存在潜在的安全漏洞,并及时修复。
- 保持警惕。 了解最新的安全漏洞,并采取相应的措施来保护你的应用程序。
安全加固示例
假设我们需要处理一些简单的数学表达式,但又不想使用 eval() 带来的风险,我们可以使用白名单加固的方式。
import operator
# 允许的运算符
operators = {
'+': operator.add,
'-': operator.sub,
'*': operator.mul,
'/': operator.truediv,
}
def safe_calculate(expression):
# 分割表达式为操作数和运算符
parts = expression.split()
if len(parts) != 3:
return "Invalid expression format"
try:
operand1 = float(parts[0])
operator_symbol = parts[1]
operand2 = float(parts[2])
except ValueError:
return "Invalid operand"
if operator_symbol not in operators:
return "Invalid operator"
# 执行安全计算
try:
result = operators[operator_symbol](operand1, operand2)
return result
except ZeroDivisionError:
return "Division by zero"
user_input = input("请输入一个简单的数学表达式 (例如:1 + 2):")
result = safe_calculate(user_input)
print("结果:", result)
这段代码将表达式分割为操作数和运算符,然后验证运算符是否在允许的列表中。如果一切正常,它会使用 operator 模块中的函数执行计算。这样可以避免执行任意代码的风险。
总结
exec() 和 eval() 是强大的 Python 内置函数,但如果使用不当,会带来严重的安全风险。为了避免代码注入漏洞,我们应该尽量避免使用 exec() 和 eval(),并寻找更安全的替代方案,例如 ast.literal_eval()、白名单验证和沙箱环境。通过采取适当的安全措施,我们可以保护我们的应用程序免受恶意攻击。
务必优先考虑安全性,并在设计和开发应用程序时采取适当的安全措施,降低风险。务必保证输入验证、最小权限原则和定期安全审查。
更多IT精英技术系列讲座,到智猿学院