この記事では、Yun Duojun が eval() の仕組みと、Python プログラムで eval() を安全かつ効果的に使用する方法を皆さんと一緒に学びます。
このセクションでは主に、eval() がどのように計算を行うかを研究します。コードが安全ではないこと、および関連するセキュリティ リスクを回避する方法。
eval() 関数のセキュリティ上の問題は、この関数を使用すると、ユーザー (またはユーザー) が任意の Python コードを動的に実行できることです。
通常、読み取られている (または書き込まれている) コードが、実行したいコードではない場合があります。ユーザーまたはその他の外部ソースからの入力を計算するために eval() を使用する必要がある場合、どのコードが実行されるかを判断することは不可能になり、これは非常に深刻なセキュリティ上の脆弱性となり、ハッカーに対して非常に脆弱になります。
一般に、eval() の使用はお勧めしません。ただし、この関数を使用する必要がある場合は、信頼できない入力ではこの関数を決して使用しないという経験則を覚えておく必要があります。このルールのポイントは、どのタイプの入力が信頼できるかを把握することです。
例として、 eval() を自由に使用すると、作成したコードが抜け穴だらけになります。任意の Python 数式 (ユーザー定義式) を評価するオンライン サービスを構築し、[実行] ボタンをクリックするとします。アプリケーション app はユーザーの入力を受け取り、それを評価のために eval() に渡します。
このアプリケーションは個人サーバー上で実行され、これらのサーバーには重要なファイルが含まれています。Linux オペレーティング システム上でコマンドを実行し、プロセスに正当な権限がある場合、悪意のあるユーザーが危険な文字列を入力する可能性があります。次のコマンドのように、サーバーに損害を与える可能性があります。
"__import__('subprocess').getoutput('rm –rf *')"
上記のコードは、プログラムの現在のディレクトリにあるすべてのファイルを削除します。これは絶対にひどいことです!
注: __import__() は、文字列の形式でモジュール名を受け取り、モジュール オブジェクトへの参照を返す組み込み関数です。 __import__() は関数であり、import ステートメントとはまったく異なります。 eval() を使用して import ステートメントを評価することはできません。
入力が信頼できない場合に eval() 関数によってもたらされるセキュリティ リスクを回避する完全に効果的な方法はありません。実際、 eval() の実行環境を制限することでリスクを軽減できます。以下では、いくつかのリスク回避テクニックを学びます。
グローバルとローカルのパラメーターにカスタム辞書を渡すことで、eval() の実行環境を制限できます。たとえば、両方の引数に空の辞書を渡すと、 eval() が呼び出し元の現在のスコープまたは名前空間内の変数名にアクセスするのを防ぐことができます。
# 避免访问调用者当前范围内的名字 >>> x = 100 >>> eval("x * 5", {}, {}) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<string>", line 1, in <module> NameError: name 'x' is not defined
空の辞書 ({}) がグローバルとローカルに渡された場合、 eval() はグローバル名前空間とローカル名前空間で文字列 "x * 5 " を計算します。名前はx。したがって、eval() は NameError をスローします。
ただし、このようにグローバルおよびローカルのパラメーターを制限しても、Python のすべての組み込み変数名に引き続きアクセスできるため、Python の eval() の使用に関連するセキュリティ リスクがすべて排除されるわけではありません。
関数 eval() は、式を解析する前に、builtins 組み込みモジュール ディクショナリの参照をグローバルに自動的に挿入します。組み込み関数 __import__() を使用して、標準ライブラリおよびシステムにインストールされているサードパーティ モジュールにアクセスします。これは、悪意のあるユーザーによって簡単に悪用される可能性もあります。
次の例は、グローバルとローカルを制限した後でも、任意の組み込み関数と、数学やサブプロセスなどの任意の標準モジュールを使用できることを示しています。
>>> eval("sum([5, 5, 5])", {}, {}) 15 >>> eval("__import__('math').sqrt(25)", {}, {}) 5.0 >>> eval("__import__('subprocess').getoutput('echo Hello, World')", {}, {}) 'Hello, World'
__import__() を使用して、数学やサブプロセスのインポートなど、標準またはサードパーティのモジュールをインポートできます。したがって、数学、サブプロセス、またはその他のモジュールで定義された関数やクラスにはアクセスできます。ここで、悪意のあるユーザーが標準ライブラリのサブプロセスやその他の強力なモジュールを使用してシステムに対して何を行うことができるかを想像してみてください。
このリスクを軽減するには、グローバルの「__builtins__」キーをオーバーライドすることで、Python 組み込み関数へのアクセスを制限できます。通常は、キーと値のペア「__builtins__:{}」を含むカスタム辞書を使用することをお勧めします。
>>> eval("__import__('math').sqrt(25)", {"__builtins__": {}}, {}) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<string>", line 1, in <module> NameError: name '__import__' is not defined
如果我们将一个包含键值对 "__builtins__: {}" 的字典传递给 globals,那么 eval() 就不能直接访问 Python 的内置函数,比如 __import__()。
然而这种方法仍然无法使得 eval() 完全规避风险。
即使可以使用自定义的 globals 和 locals 字典来限制 eval()的执行环境,这个函数仍然会被攻击。例如可以使用像""、"[]"、"{}"或"() "来访问类object以及一些特殊属性。
>>> "".__class__.__base__ <class 'object'> >>> [].__class__.__base__ <class 'object'> >>> {}.__class__.__base__ <class 'object'> >>> ().__class__.__base__ <class 'object'>
一旦访问了 object,可以使用特殊的方法 `.__subclasses__()`[1] 来访问所有继承于 object 的类。下面是它的工作原理。
>>> for sub_class in ().__class__.__base__.__subclasses__(): ... print(sub_class.__name__) ... type weakref weakcallableproxy weakproxy int ...
这段代码将打印出一个大类列表。其中一些类的功能非常强大,因此也是一个重要的安全漏洞,而且我们无法通过简单地限制 eval() 的避免该漏洞。
>>> input_string = """[ ... c for c in ().__class__.__base__.__subclasses__() ... if c.__name__ == "range" ... ][0](10 "0")""" >>> list(eval(input_string, {"__builtins__": {}}, {})) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
上面代码中的列表推导式对继承自 object 的类进行过滤,返回一个包含 range 类的 list。第一个索引([0])返回类的范围。一旦获得了对 range 的访问权,就调用它来生成一个 range 对象。然后在 range 对象上调用 list(),从而生成一个包含十个整数的列表。
在这个例子中,用 range 来说明 eval() 函数中的一个安全漏洞。现在想象一下,如果你的系统暴露了像 subprocess.Popen 这样的类,一个恶意的用户可以做什么?
我们或许可以通过限制输入中的名字的使用,从而解决这个漏洞。该技术涉及以下步骤。
看看下面这个函数,我们在其中实现了所有这些步骤。
>>> def eval_expression(input_string): ... # Step 1 ... allowed_names = {"sum": sum} ... # Step 2 ... code = compile(input_string, "<string>", "eval") ... # Step 3 ... for name in code.co_names: ... if name not in allowed_names: ... # Step 4 ... raise NameError(f"Use of {name} not allowed") ... return eval(code, {"__builtins__": {}}, allowed_names)
eval_expression() 函数可以在 eval() 中使用的名字限制为字典 allowed_names 中的那些名字。而该函数使用了 .co_names,它是代码对象的一个属性,返回一个包含代码对象中的名字的元组。
下面的例子显示了eval_expression() 在实践中是如何工作的。
>>> eval_expression("3 + 4 * 5 + 25 / 2") 35.5 >>> eval_expression("sum([1, 2, 3])") 6 >>> eval_expression("len([1, 2, 3])") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 10, in eval_expression NameError: Use of len not allowed >>> eval_expression("pow(10, 2)") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 10, in eval_expression NameError: Use of pow not allowed
如果调用 eval_expression() 来计算算术运算,或者使用包含允许的变量名的表达式,那么将会正常运行并得到预期的结果,否则会抛出一个`NameError`。上面的例子中,我们仅允许输入的唯一名字是sum(),而不允许其他算术运算名称如len()和pow(),所以当使用它们时,该函数会产生一个`NameError`。
如果完全不允许使用名字,那么可以把 eval_expression() 改写:
>>> def eval_expression(input_string): ... code = compile(input_string, "<string>", "eval") ... if code.co_names: ... raise NameError(f"Use of names not allowed") ... return eval(code, {"__builtins__": {}}, {}) ... >>> eval_expression("3 + 4 * 5 + 25 / 2") 35.5 >>> eval_expression("sum([1, 2, 3])") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in eval_expression NameError: Use of names not allowed
现在函数不允许在输入字符串中出现任何变量名。需要检查.co_names中的变量名,一旦发现就引发 NameError。否则计算 input_string 并返回计算的结果。此时也使用一个空的字典来限制locals。
我们可以使用这种技术来尽量减少eval()的安全问题,并加强安全盔甲,防止恶意攻击。
函数eval()的一个常见用例是计算包含标准Python字面符号的字符串,并将其变成具体的对象。
标准库提供了一个叫做 literal_eval()[2] 的函数,可以帮助实现这个目标。虽然这个函数不支持运算符,但它支持 list, tuples, numbers, strings等等。
>>> from ast import literal_eval >>> #计算字面意义 >>> literal_eval("15.02") 15.02 >>> literal_eval("[1, 15]") [1, 15] >>> literal_eval("(1, 15)") (1, 15) >>> literal_eval("{'one': 1, 'two': 2}") {'one': 1, 'two': 2} >>> # 试图计算一个表达式 >>> literal_eval("sum([1, 15]) + 5 + 8 * 2") Traceback (most recent call last): ... ValueError: malformed node or string: <_ast.BinOp object at 0x7faedecd7668>
注意,literal_eval()只作用于标准类型的字词。它不支持使用运算符或变量名。如果向 literal_eval() 传递一个表达式,会得到一个 ValueError。这个函数还可以将与使用eval()有关的安全风险降到最低。
在 Python 3.x 中,内置函数 input() 读取命令行上的用户输入,去掉尾部的换行,转换为字符串,并将结果返回给调用者。由于 input() 的输出结果是一个字符串,可以把它传递给 eval() 并作为一个 Python 表达式来计算它。
>>> eval(input("Enter a math expression: ")) Enter a math expression: 15 * 2 30 >>> eval(input("Enter a math expression: ")) Enter a math expression: 5 + 8 13
我们可以将函数 eval() 包裹在函数 input() 中,实现自动计算用户的输入的功能。一个常见用例模拟 Python 2.x 中 input() 的行为,input() 将用户的输入作为一个 Python 表达式来计算,并返回结果。
因为它涉及安全问题,因此在 Python 2.x 中的 input() 的这种行为在 Python 3.x 中被改变了。
到目前为止,我们已经了解了函数 eval() 是如何工作的以及如何在实践中使用它。此外还了解到 eval() 具有重要的安全漏洞,尽量在代码中避免使用 eval(),然而在某些情况下,eval() 可以为我们节省大量的时间和精力。因此,学会合理使用 eval() 函数还是蛮重要的。
在本节中,将编写一个应用程序来动态地计算数学表达式。首先不使用eval()来解决这个问题,那么需要通过以下步骤:
考虑到 Python 可以处理和计算的各种表达式非常耗时。其实我们可以使用 eval() 来解决这个问题,而且通过上文我们已经学会了几种技术来规避相关的安全风险。
首先创建一个新的Python脚本,名为mathrepl.py,然后添加以下代码。
import math __version__ = "1.0" ALLOWED_NAMES = { k: v for k, v in math.__dict__.items() if not k.startswith("__") } PS1 = "mr>>" WELCOME = f""" MathREPL {__version__}, your Python math expressions evaluator! Enter a valid math expression after the prompt "{PS1}". Type "help" for more information. Type "quit" or "exit" to exit. """ USAGE = f""" Usage: Build math expressions using numeric values and operators. Use any of the following functions and constants: {', '.join(ALLOWED_NAMES.keys())} """
在这段代码中,我们首先导入 math 模块。这个模块使用预定义的函数和常数进行数学运算。常量 ALLOWED_NAMES 保存了一个包含数学中非特变量名的字典。这样就可以用 eval() 来使用它们。
我们还定义了另外三个字符串常量。将使用它们作为脚本的用户界面,并根据需要打印到屏幕上。
现在准备编写核心功能,首先编写一个函数,接收数学表达式作为输入,并返回其结果。此外还需要写一个叫做 evaluate() 的函数,如下所示。
def evaluate(expression): """Evaluate a math expression.""" # 编译表达式 code = compile(expression, "<string>", "eval") # 验证允许名称 for name in code.co_names: if name not in ALLOWED_NAMES: raise NameError(f"The use of '{name}' is not allowed") return eval(code, {"__builtins__": {}}, ALLOWED_NAMES)
以下是该功能的工作原理。
注意: 由于这个应用程序使用了 math 中定义的函数,需要注意,当我们用一个无效的输入值调用这些函数时,其中一些函数将抛出 ValueError 异常。
例如,math.sqrt(-10) 会引发一个异常,因为-10的平方根是未定义的。我们会在稍后的代码中看到如何捕捉该异常。
为 globals 和 locals 参数使用自定义值,加上名称检查,可以将与使用eval()有关的安全风险降到最低。
当在 main() 中编写其代码时,数学表达式计算器就完成了。在这个函数中,定义程序的主循环,结束读取和计算用户在命令行中输入的表达式的循环。
在这个例子中,应用程序将:
def main(): """Main loop: Read and evaluate user's input.""" print(WELCOME) while True: #读取用户的输入 try: expression = input(f"{PS1} ") except (KeyboardInterrupt, EOFError): raise SystemExit() # 处理特殊命令 if expression.lower() == "help": print(USAGE) continue if expression.lower() in {"quit", "exit"}: raise SystemExit() # 对表达式进行计算并处理错误 try: result = evaluate(expression) except SyntaxError: # 如果用户输入了一个无效的表达式 print("Invalid input expression syntax") continue except (NameError, ValueError) as err: # 如果用户试图使用一个不允许的名字 # 对于一个给定的数学函数来说是一个无效的值 print(err) continue # 如果没有发生错误,则打印结果 print(f"The result is: {result}") if __name__ == "__main__": main()
在main()中,首先打印WELCOME消息。然后在一个try语句中读取用户的输入,以捕获键盘中断和 EOFError。如果这些异常发生,就终止应用程序。
如果用户输入帮助选项,那么应用程序就会显示使用指南。同样地,如果用户输入quit或exit,那么应用程序就会终止。
最后,使用evaluate()来计算用户的数学表达式,然后将结果打印到屏幕上。值得注意的是,对 evaluate() 的调用会引发以下异常。
注意,在main()中,捕捉了所有已知异常,并相应地打印信息给用户。这将使用户能够审查表达式,修复问题,并再次运行程序。
现在已经使用函数 eval() 在大约七十行的代码中建立了一个数学表达式计算器。要运行这个程序,打开我们的系统命令行,输入以下命令。
$ python3 mathrepl.py
这个命令将启动数学表达式计算器的命令行界面(CLI),会在屏幕上看到类似这样的东西。
MathREPL 1.0, your Python math expressions evaluator! Enter a valid math expression after the prompt "mr>>". Type "help" for more information. Type "quit" or "exit" to exit. mr>>
现在我们可以输入并计算任何数学表达式。例如,输入以下表达式。
mr>> 25 * 2 The result is: 50 mr>> sqrt(25) The result is: 5.0 mr>> pi The result is: 3.141592653589793
如果输入了一个有效的数学表达式,那么应用程序就会对其进行计算,并将结果打印到屏幕上。如果表达式有任何问题,那么应用程序会告诉我们。
mr>> 5 * (25 + 4 Invalid input expression syntax mr>> sum([1, 2, 3, 4, 5]) The use of 'sum' is not allowed mr>> sqrt(-15) math domain error mr>> factorial(-15) factorial() not defined for negative values
在第一个示例中,漏掉了右括号,因此收到一条消息,告诉我们语法不正确。然后调用 sum() ,这会得到一个解释性的异常消息。最后,使用无效的输入值调用“math”函数,应用程序将生成一条消息来识别输入中的问题。
你可以使用Python的 eval() 从基于字符串或基于代码的输入中计算Python 表达式。当我们动态地计算Python表达式,并希望避免从头创建自己的表达式求值器的麻烦时,这个内置函数可能很有用。
在本文中,我们已经学习了 eval() 是如何工作的,以及如何安全有效地使用它来计算任意Python表达式。
以上がPython eval 関数は数式計算機を構築しますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。