Python eval 函数构建数学表达式计算器(1)

上一篇 / 下一篇  2022-09-09 10:48:14

  2022软件测试行业调查报告开始了,点击链接http://vote.51testing.com/ 填写问卷,五门测试实战课程任选两门免费学习。同时还有转发朋友圈免费领数据线的活动。快来参加吧~
  Python 中的函数eval()?是一个非常有用的工具,在前期,我们一起学习过该函数点击查看??:Python eval 函数动态地计算数学表达式?。尽管如此,我们在使用之前,还需要考虑到该函数的一些重要的安全问题。在本文中,云朵君将和大家一起学习 eval() 如何工作,以及如何在 Python 程序中安全有效地使用它。
  ·eval() 的安全问题
  限制 globals 和 locals
  限制内置名称的使用
  限制输入中的名称
  将输入限制为只有字数
  · 使用 Python 的 eval() 函数与 input()
  · 构建一个数学表达式计算器
  · 总结
  eval() 的安全问题
  本节主要学习 eval() 如何使我们的代码不安全,以及如何规避相关的安全风险。
  eval() 函数的安全问题在于它允许你(或你的用户)动态地执行任意的Python代码。
  通常情况下,会存在正在读(或写)的代码不是我们要执行的代码的情况。如果我们需要使用eval()来计算来自用户或任何其他外部来源的输入,此时将无法确定哪些代码将被执行,这将是一个非常严重的安全漏洞,极易收到黑客的攻击。
  一般情况下,我们并不建议使用 eval()。但如果非要使用该函数,需要记住根据经验法则:永远不要 用 未经信任的输入 来使用该函数。这条规则的重点在于要弄清楚我们可以信任哪些类型的输入。
  举个例子说明,随意使用eval()?会使我们写的代码漏洞百出。假设你想建立一个在线服务来计算任意的Python数学表达式:用户自定义表达式,然后点击运行?按钮。应用程序app获得用户的输入并将其传递给eval()进行计算。
  这个应用程序app将在我们的个人服务器上运行,而那些服务器内具有重要文件,如果你在一个Linux 操作系统运行命令,并且该进程有合法权限,那么恶意的用户可以输入危险的字符串而损害服务器,比如下面这个命令。
  "__import__('subprocess').getoutput('rm –rf *')"
  上述代码将删除程序当前目录中的所有文件。这简直太可怕了!
  注意: __import__()?是一个内置函数,它接收一个字符串形式的模块名称,并返回一个模块对象的引用。__import__()? 是一个函数,它与导入语句完全不同。我们不能使用 eval() 来计算一个导入语句。
  当输入不受信任时,并没有完全有效的方法来避免eval()?函数带来的安全风险。其实我们可以通过限制eval()的执行环境来减少风险。在下面的内容中,我们学习一些规避风险的技巧。
  限制 globals 和 locals
  可以通过向 globals 和 locals 参数传递自定义字典来限制 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
  如果给 globals 和 locals 传递了空的字典({}?),那么eval()?在计算字符串 "x * 5 "? 时,在它的全局名字空间和局部名字空间都找不到名字x?。因此,eval()将抛出一个NameError。
  然而,像这样限制 globals 和 locals 参数并不能消除与使用 Python 的 eval() 有关的所有安全风险,因为仍然可以访问所有 Python 的内置变量名。
  限制内置名称的使用
  函数 eval()? 会在解析 expression 之前自动将 builtins? 内置模块字典的引用插入到 globals 中。使用内置函数 __import__()  来访问标准库和在系统上安装的任何第三方模块。这还容易被恶意用户利用。
  下面的例子表明,即使在限制了 globals 和 locals 之后,我们也可以使用任何内置函数和任何标准模块,如 math 或 subprocess。
  >>> eval("sum([5, 5, 5])", {}, {})
  15
  >>> eval("__import__('math').sqrt(25)", {}, {})
  5.0
  >>> eval("__import__('subprocess').getoutput('echo Hello, World')", {}, {})
  'Hello, World'
  我们可以使用 __import__() 来导入任何标准或第三方模块,如导入 math 和 subprocess 。因此 可以访问在 math、subprocess 或任何其他模块中定义的任何函数或类。现在想象一下,一个恶意的用户可以使用 subprocess 或标准库中任何其他强大的模块对系统做什么,那就有点恐怖了。
  为了减少这种风险,可以通过覆盖 globals 中的 "__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 这样的类,一个恶意的用户可以做什么?
  我们或许可以通过限制输入中的名字的使用,从而解决这个漏洞。该技术涉及以下步骤。
  ·创建一个包含你想用eval()使用的名字的字典。
  · 在eval? 模式下使用compile() 将输入字符串编译为字节码。
  · 检查字节码对象上的.co_names,以确保它只包含允许的名字。
  · 如果用户试图输入一个不允许的名字,会引发一个`NameError`。
  看看下面这个函数,我们在其中实现了所有这些步骤。
  >>> 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()的安全问题,并加强安全盔甲,防止恶意攻击。
  2022软件测试行业调查报告开始了,点击链接http://vote.51testing.com/ 填写问卷,五门测试实战课程任选两门免费学习。同时还有转发朋友圈免费领数据线的活动。快来参加吧~

TAG: 软件开发 Python

 

评分:0

我来说两句

Open Toolbar