利用Python+eval函数构建数学表达式计算器_第1页
利用Python+eval函数构建数学表达式计算器_第2页
利用Python+eval函数构建数学表达式计算器_第3页
利用Python+eval函数构建数学表达式计算器_第4页
利用Python+eval函数构建数学表达式计算器_第5页
已阅读5页,还剩11页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第利用Python+eval函数构建数学表达式计算器目录eval()的安全问题限制globals和locals限制内置名称的使用限制输入中的名称将输入限制为只有字数使用eval()与input()函数构建一个数学表达式计算器总结Python中的函数eval()​是一个非常有用的工具,在前期,我们一起学习过该函数点击查看:Pythoneval函数动态地计算数学表达式​。尽管如此,我们在使用之前,还需要考虑到该函数的一些重要的安全问题。在本文中,云朵君将和大家一起学习eval()如何工作,以及如何在Python程序中安全有效地使用它。

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(mostrecentcalllast):

File"stdin",line1,inmodule

File"string",line1,inmodule

NameError:name'x'isnotdefined

如果给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])",{},{})

eval("__import__('math').sqrt(25)",{},{})

eval("__import__('subprocess').getoutput('echoHello,World')",{},{})

'Hello,World'

我们可以使用__import__()来导入任何标准或第三方模块,如导入math和subprocess。因此可以访问在math、subprocess或任何其他模块中定义的任何函数或类。现在想象一下,一个恶意的用户可以使用subprocess或标准库中任何其他强大的模块对系统做什么,那就有点恐怖了。

为了减少这种风险,可以通过覆盖globals中的__builtins__​键来限制对Python内置函数的访问。通常建议使用一个包含键值对__builtins__:{}的自定义字典。

eval("__import__('math').sqrt(25)",{"__builtins__":{}},{})

Traceback(mostrecentcalllast):

File"stdin",line1,inmodule

File"string",line1,inmodule

NameError:name'__import__'isnotdefined

如果我们将一个包含键值对__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__()`来访问所有继承于object的类。下面是它的工作原理。

forsub_classin().__class__.__base__.__subclasses__():

...print(sub_class.__name__)

weakref

weakcallableproxy

weakproxy

...

这段代码将打印出一个大类列表。其中一些类的功能非常强大,因此也是一个重要的安全漏洞,而且我们无法通过简单地限制eval()的避免该漏洞。

input_string="""[

...cforcin().__class__.__base__.__subclasses__()

...ifc.__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`。

看看下面这个函数,我们在其中实现了所有这些步骤。

defeval_expression(input_string):

...#Step1

...allowed_names={"sum":sum}

...#Step2

...code=compile(input_string,"string","eval")

...#Step3

...fornameincode.co_names:

...ifnamenotinallowed_names:

...#Step4

...raiseNameError(f"Useof{name}notallowed")

...returneval(code,{"__builtins__":{}},allowed_names)

eval_expression()​函数可以在eval()​中使用的名字限制为字典allowed_names​中的那些名字。而该函数使用了.co_names,它是代码对象的一个属性,返回一个包含代码对象中的名字的元组。

下面的例子显示了eval_expression()在实践中是如何工作的。

eval_expression("3+4*5+25/2")

eval_expression("sum([1,2,3])")

eval_expression("len([1,2,3])")

Traceback(mostrecentcalllast):

File"stdin",line1,inmodule

File"stdin",line10,ineval_expression

NameError:Useoflennotallowed

eval_expression("pow(10,2)")

Traceback(mostrecentcalllast):

File"stdin",line1,inmodule

File"stdin",line10,ineval_expression

NameError:Useofpownotallowed

如果调用eval_expression()​来计算算术运算,或者使用包含允许的变量名的表达式,那么将会正常运行并得到预期的结果,否则会抛出一个`NameError`。上面的例子中,我们仅允许输入的唯一名字是sum()​,而不允许其他算术运算名称如len()和pow(),所以当使用它们时,该函数会产生一个`NameError`。

如果完全不允许使用名字,那么可以把eval_expression()改写:

defeval_expression(input_string):

...code=compile(input_string,"string","eval")

...ifcode.co_names:

...raiseNameError(f"Useofnamesnotallowed")

...returneval(code,{"__builtins__":{}},{})

eval_expression("3+4*5+25/2")

eval_expression("sum([1,2,3])")

Traceback(mostrecentcalllast):

File"stdin",line1,inmodule

File"stdin",line4,ineval_expression

NameError:Useofnamesnotallowed

现在函数不允许在输入字符串中出现任何变量名。需要检查.co_names​中的变量名,一旦发现就引发NameError。否则计算input_string​并返回计算的结果。此时也使用一个空的字典来限制locals。

我们可以使用这种技术来尽量减少eval()的安全问题,并加强安全盔甲,防止恶意攻击。

将输入限制为只有字数

函数eval()的一个常见用例是计算包含标准Python字面符号的字符串,并将其变成具体的对象。

标准库提供了一个叫做literal_eval()的函数,可以帮助实现这个目标。虽然这个函数不支持运算符,但它支持list,tuples,numbers,strings等等。

fromastimportliteral_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(mostrecentcalllast):

ValueError:malformednodeorstring:_ast.BinOpobjectat0x7faedecd7668

注意,literal_eval()​只作用于标准类型的字词。它不支持使用运算符或变量名。如果向literal_eval()​传递一个表达式,会得到一个ValueError。这个函数还可以将与使用eval()有关的安全风险降到最低。

使用eval()与input()函数

在Python3.x中,内置函数input()读取命令行上的用户输入,去掉尾部的换行,转换为字符串,并将结果返回给调用者。由于input()​的输出结果是一个字符串,可以把它传递给eval()并作为一个Python表达式来计算它。

eval(input("Enteramathexpression:"))

Enteramathexpression:15*2

eval(input("Enteramathexpression:"))

Enteramathexpression:5+8

13

我们可以将函数eval()​包裹在函数input()​中,实现自动计算用户的输入的功能。一个常见用例模拟Python2.x中input()​的行为,input()将用户的输入作为一个Python表达式来计算,并返回结果。

因为它涉及安全问题,因此在Python2.x中的input()的这种行为在Python3.x中被改变了。

构建一个数学表达式计算器

到目前为止,我们已经了解了函数eval()​是如何工作的以及如何在实践中使用它。此外还了解到eval()​具有重要的安全漏洞,尽量在代码中避免使用eval()​,然而在某些情况下,eval()​可以为我们节省大量的时间和精力。因此,学会合理使用eval()函数还是蛮重要的。

在本节中,将编写一个应用程序来动态地计算数学表达式。首先不使用eval()来解决这个问题,那么需要通过以下步骤:

解析输入的表达式。将表达式的组成部分变为Python对象(数字、运算符、函数等等)。将所有的东西合并成一个表达式。确认该表达式在Python中是有效的。计算最终表达式并返回结果。

考虑到Python可以处理和计算的各种表达式非常耗时。其实我们可以使用eval()来解决这个问题,而且通过上文我们已经学会了几种技术来规避相关的安全风险。

首先创建一个新的Python脚本,名为mathrepl.py,然后添加以下代码。

importmath

__version__="1.0"

ALLOWED_NAMES={

k:vfork,vinmath.__dict__.items()ifnotk.startswith("__")

PS1="mr"

WELCOME=f"""

MathREPL{__version__},yourPythonmathexpressionsevaluator!

Enteravalidmathexpressionaftertheprompt"{PS1}".

Type"help"formoreinformation.

Type"quit"or"exit"toexit.

USAGE=f"""

Usage:

Buildmathexpressionsusingnumericvaluesandoperators.

Useanyofthefollowingfunctionsandconstants:

{','.join(ALLOWED_NAMES.keys())}

"""

在这段代码中,我们首先导入math模块。这个模块使用预定义的函数和常数进行数学运算。常量ALLOWED_NAMES​保存了一个包含数学中非特变量名的字典。这样就可以用eval()来使用它们。

我们还定义了另外三个字符串常量。将使用它们作为脚本的用户界面,并根据需要打印到屏幕上。

现在准备编写核心功能,首先编写一个函数,接收数学表达式作为输入,并返回其结果。此外还需要写一个叫做evaluate()的函数,如下所示。

defevaluate(expression):

"""Evaluateamathexpression."""

#编译表达式

code=compile(expression,"string","eval")

#验证允许名称

fornameincode.co_names:

ifnamenotinALLOWED_NAMES:

raiseNameError(f"Theuseof'{name}'isnotallowed")

returneval(code,{"__builtins__":{}},ALLOWED_NAMES)

以下是该功能的工作原理。

定义了evaluate(),该函数将字符串表达式作为参数,并返回一个浮点数,代表将字符串作为数学表达式进行计算的结果。使用compile()将输入的字符串表达式变成编译的Python代码。如果用户输入了一个无效的表达式,编译操作将引发一个SyntaxError。使用一个for循环,检查表达式中包含的名字,并确认它们可以在最终表达式中使用。如果用户提供的名字不在允许的名字列表中,那么会引发一个NameError。执行数学表达式的实际计算。注意将自定义的字典传递给了globals和locals。ALLOWED_NAMES保存了数学中定义的函数和常量。

注意:由于这个应用程序使用了math中定义的函数,需要注意,当我们用一个无效的输入值调用这些函数时,其中一些函数将抛出ValueError异常。

例如,math.sqrt(-10)​会引发一个异常,因为-10的平方根是未定义的。我们会在稍后的代码中看到如何捕捉该异常。

为globals和locals参数使用自定义值,加上名称检查,可以将与使用eval()有关的安全风险降到最低。

当在main()中编写其代码时,数学表达式计算器就完成了。在这个函数中,定义程序的主循环,结束读取和计算用户在命令行中输入的表达式的循环。

在这个例子中,应用程序将:

向用户打印一条欢迎信息显示一个提示,准备读取用户的输入提供获取使用说明和终止应用程序的选项读取用户的数学表达式计算用户的数学表达式将计算的结果打印到屏幕上

defmain():

"""Mainloop:Readandevaluateuser'sinput."""

print(WELCOME)

whileTrue:

#读取用户的输入

try:

expression=input(f"{PS1}")

except(KeyboardInterrupt,EOFError):

raiseSystemExit()

#处理特殊命令

ifexpression.lower()=="help":

print(USAGE)

continue

ifexpression.lower()in{"quit","exit"}:

raiseSystemExit()

#对表达式进行计算并处理错误

try:

result=evaluate(expression)

exceptSyntaxError:

#如果用户输入了一个无效的表达式

print("Invalidinputexpressionsyntax")

continue

except(NameError,ValueError)aserr:

#如果用户试图使用一个不允许的名字

#对于一个给定的数学函数来说是一个无效的值

print(err)

continue

#如果没有发生错误,则打印结果

print(f"Theresultis:{result}")

if__name__=="__main__":

main()

在main()​中,首先打印WELCOME消息。然后在一个try语句中读取用户的输入,以捕获键盘中断和EOFError。如果这些异常发生,就终止应用程序。

如果用户输入帮助选项,那么应用程序就会显示使用指南。同样地,如果用户输入quit或exit,那么应用程序就会终止。

最后,使用evaluate()​来计算用户的数学表达式,然后将结果打印到屏幕上。值得注意的是,对evaluate()的调用会引发以下异常。

SyntaxError:语法错误,当用户输入一个不符合Python语法的表达式时,就会发生这种情况。NameError:当用户试图使用一个不允许的名称(函数、类或属性)时,就会发生这种情况。ValueError:当用户试图使用一个不允许的值作为数学中某个函数的输入时,就会发生这种情况。

注意,在main()中,捕捉了所有已知异常,并相应地打印信息给用户。这将使用户能够审查表

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论