利用Python+eval函數(shù)構(gòu)建數(shù)學(xué)表達(dá)式計算器_第1頁
利用Python+eval函數(shù)構(gòu)建數(shù)學(xué)表達(dá)式計算器_第2頁
利用Python+eval函數(shù)構(gòu)建數(shù)學(xué)表達(dá)式計算器_第3頁
利用Python+eval函數(shù)構(gòu)建數(shù)學(xué)表達(dá)式計算器_第4頁
利用Python+eval函數(shù)構(gòu)建數(shù)學(xué)表達(dá)式計算器_第5頁
已閱讀5頁,還剩11頁未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)

文檔簡介

第利用Python+eval函數(shù)構(gòu)建數(shù)學(xué)表達(dá)式計算器目錄eval()的安全問題限制globals和locals限制內(nèi)置名稱的使用限制輸入中的名稱將輸入限制為只有字?jǐn)?shù)使用eval()與input()函數(shù)構(gòu)建一個數(shù)學(xué)表達(dá)式計算器總結(jié)Python中的函數(shù)eval()?是一個非常有用的工具,在前期,我們一起學(xué)習(xí)過該函數(shù)點(diǎn)擊查看:Pythoneval函數(shù)動態(tài)地計算數(shù)學(xué)表達(dá)式?。盡管如此,我們在使用之前,還需要考慮到該函數(shù)的一些重要的安全問題。在本文中,云朵君將和大家一起學(xué)習(xí)eval()如何工作,以及如何在Python程序中安全有效地使用它。

eval()的安全問題

本節(jié)主要學(xué)習(xí)eval()如何使我們的代碼不安全,以及如何規(guī)避相關(guān)的安全風(fēng)險。

eval()函數(shù)的安全問題在于它允許你(或你的用戶)動態(tài)地執(zhí)行任意的Python代碼。

通常情況下,會存在正在讀(或?qū)懀┑拇a不是我們要執(zhí)行的代碼的情況。如果我們需要使用eval()來計算來自用戶或任何其他外部來源的輸入,此時將無法確定哪些代碼將被執(zhí)行,這將是一個非常嚴(yán)重的安全漏洞,極易收到黑客的攻擊。

一般情況下,我們并不建議使用eval()。但如果非要使用該函數(shù),需要記住根據(jù)經(jīng)驗(yàn)法則:永遠(yuǎn)不要用未經(jīng)信任的輸入來使用該函數(shù)。這條規(guī)則的重點(diǎn)在于要弄清楚我們可以信任哪些類型的輸入。

舉個例子說明,隨意使用eval()?會使我們寫的代碼漏洞百出。假設(shè)你想建立一個在線服務(wù)來計算任意的Python數(shù)學(xué)表達(dá)式:用戶自定義表達(dá)式,然后點(diǎn)擊運(yùn)行?按鈕。應(yīng)用程序app獲得用戶的輸入并將其傳遞給eval()進(jìn)行計算。

這個應(yīng)用程序app將在我們的個人服務(wù)器上運(yùn)行,而那些服務(wù)器內(nèi)具有重要文件,如果你在一個Linux操作系統(tǒng)運(yùn)行命令,并且該進(jìn)程有合法權(quán)限,那么惡意的用戶可以輸入危險的字符串而損害服務(wù)器,比如下面這個命令。

"__import__('subprocess').getoutput('rm–rf*')"

上述代碼將刪除程序當(dāng)前目錄中的所有文件。這簡直太可怕了!

注意:__import__()?是一個內(nèi)置函數(shù),它接收一個字符串形式的模塊名稱,并返回一個模塊對象的引用。__import__()?是一個函數(shù),它與導(dǎo)入語句完全不同。我們不能使用eval()來計算一個導(dǎo)入語句。

當(dāng)輸入不受信任時,并沒有完全有效的方法來避免eval()?函數(shù)帶來的安全風(fēng)險。其實(shí)我們可以通過限制eval()的執(zhí)行環(huán)境來減少風(fēng)險。在下面的內(nèi)容中,我們學(xué)習(xí)一些規(guī)避風(fēng)險的技巧。

限制globals和locals

可以通過向globals和locals參數(shù)傳遞自定義字典來限制eval()?的執(zhí)行環(huán)境。例如,可以給這兩個參數(shù)傳遞空的字典,以防止eval()訪問調(diào)用者當(dāng)前范圍或命名空間中的變量名。

#避免訪問調(diào)用者當(dāng)前范圍內(nèi)的名字

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參數(shù)并不能消除與使用Python的eval()有關(guān)的所有安全風(fēng)險,因?yàn)槿匀豢梢栽L問所有Python的內(nèi)置變量名。

限制內(nèi)置名稱的使用

函數(shù)eval()?會在解析expression之前自動將builtins?內(nèi)置模塊字典的引用插入到globals中。使用內(nèi)置函數(shù)__import__()來訪問標(biāo)準(zhǔn)庫和在系統(tǒng)上安裝的任何第三方模塊。這還容易被惡意用戶利用。

下面的例子表明,即使在限制了globals和locals之后,我們也可以使用任何內(nèi)置函數(shù)和任何標(biāo)準(zhǔn)模塊,如math或subprocess。

eval("sum([5,5,5])",{},{})

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

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

'Hello,World'

我們可以使用__import__()來導(dǎo)入任何標(biāo)準(zhǔn)或第三方模塊,如導(dǎo)入math和subprocess。因此可以訪問在math、subprocess或任何其他模塊中定義的任何函數(shù)或類。現(xiàn)在想象一下,一個惡意的用戶可以使用subprocess或標(biāo)準(zhǔn)庫中任何其他強(qiáng)大的模塊對系統(tǒng)做什么,那就有點(diǎn)恐怖了。

為了減少這種風(fēng)險,可以通過覆蓋globals中的__builtins__?鍵來限制對Python內(nèi)置函數(shù)的訪問。通常建議使用一個包含鍵值對__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的內(nèi)置函數(shù),比如__import__()。

然而這種方法仍然無法使得eval()完全規(guī)避風(fēng)險。

限制輸入中的名稱

即使可以使用自定義的globals?和locals?字典來限制eval()?的執(zhí)行環(huán)境,這個函數(shù)仍然會被攻擊。例如可以使用像、[]、{}或()?來訪問類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

...

這段代碼將打印出一個大類列表。其中一些類的功能非常強(qiáng)大,因此也是一個重要的安全漏洞,而且我們無法通過簡單地限制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]

上面代碼中的列表推導(dǎo)式對繼承自object?的類進(jìn)行過濾,返回一個包含range?類的list?。第一個索引([0]?)返回類的范圍。一旦獲得了對range?的訪問權(quán),就調(diào)用它來生成一個range?對象。然后在range?對象上調(diào)用list(),從而生成一個包含十個整數(shù)的列表。

在這個例子中,用range?來說明eval()?函數(shù)中的一個安全漏洞?,F(xiàn)在想象一下,如果你的系統(tǒng)暴露了像subprocess.Popen這樣的類,一個惡意的用戶可以做什么?

我們或許可以通過限制輸入中的名字的使用,從而解決這個漏洞。該技術(shù)涉及以下步驟。

創(chuàng)建一個包含你想用eval()使用的名字的字典。在eval?模式下使用compile()將輸入字符串編譯為字節(jié)碼。檢查字節(jié)碼對象上的.co_names,以確保它只包含允許的名字。如果用戶試圖輸入一個不允許的名字,會引發(fā)一個`NameError`。

看看下面這個函數(shù),我們在其中實(shí)現(xiàn)了所有這些步驟。

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()?函數(shù)可以在eval()?中使用的名字限制為字典allowed_names?中的那些名字。而該函數(shù)使用了.co_names,它是代碼對象的一個屬性,返回一個包含代碼對象中的名字的元組。

下面的例子顯示了eval_expression()在實(shí)踐中是如何工作的。

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

如果調(diào)用eval_expression()?來計算算術(shù)運(yùn)算,或者使用包含允許的變量名的表達(dá)式,那么將會正常運(yùn)行并得到預(yù)期的結(jié)果,否則會拋出一個`NameError`。上面的例子中,我們僅允許輸入的唯一名字是sum()?,而不允許其他算術(shù)運(yùn)算名稱如len()和pow(),所以當(dāng)使用它們時,該函數(shù)會產(chǎn)生一個`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

現(xiàn)在函數(shù)不允許在輸入字符串中出現(xiàn)任何變量名。需要檢查.co_names?中的變量名,一旦發(fā)現(xiàn)就引發(fā)NameError。否則計算input_string?并返回計算的結(jié)果。此時也使用一個空的字典來限制locals。

我們可以使用這種技術(shù)來盡量減少eval()的安全問題,并加強(qiáng)安全盔甲,防止惡意攻擊。

將輸入限制為只有字?jǐn)?shù)

函數(shù)eval()的一個常見用例是計算包含標(biāo)準(zhǔn)Python字面符號的字符串,并將其變成具體的對象。

標(biāo)準(zhǔn)庫提供了一個叫做literal_eval()的函數(shù),可以幫助實(shí)現(xiàn)這個目標(biāo)。雖然這個函數(shù)不支持運(yùn)算符,但它支持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}

#試圖計算一個表達(dá)式

literal_eval("sum([1,15])+5+8*2")

Traceback(mostrecentcalllast):

ValueError:malformednodeorstring:_ast.BinOpobjectat0x7faedecd7668

注意,literal_eval()?只作用于標(biāo)準(zhǔn)類型的字詞。它不支持使用運(yùn)算符或變量名。如果向literal_eval()?傳遞一個表達(dá)式,會得到一個ValueError。這個函數(shù)還可以將與使用eval()有關(guān)的安全風(fēng)險降到最低。

使用eval()與input()函數(shù)

在Python3.x中,內(nèi)置函數(shù)input()讀取命令行上的用戶輸入,去掉尾部的換行,轉(zhuǎn)換為字符串,并將結(jié)果返回給調(diào)用者。由于input()?的輸出結(jié)果是一個字符串,可以把它傳遞給eval()并作為一個Python表達(dá)式來計算它。

eval(input("Enteramathexpression:"))

Enteramathexpression:15*2

eval(input("Enteramathexpression:"))

Enteramathexpression:5+8

13

我們可以將函數(shù)eval()?包裹在函數(shù)input()?中,實(shí)現(xiàn)自動計算用戶的輸入的功能。一個常見用例模擬Python2.x中input()?的行為,input()將用戶的輸入作為一個Python表達(dá)式來計算,并返回結(jié)果。

因?yàn)樗婕鞍踩珕栴},因此在Python2.x中的input()的這種行為在Python3.x中被改變了。

構(gòu)建一個數(shù)學(xué)表達(dá)式計算器

到目前為止,我們已經(jīng)了解了函數(shù)eval()?是如何工作的以及如何在實(shí)踐中使用它。此外還了解到eval()?具有重要的安全漏洞,盡量在代碼中避免使用eval()?,然而在某些情況下,eval()?可以為我們節(jié)省大量的時間和精力。因此,學(xué)會合理使用eval()函數(shù)還是蠻重要的。

在本節(jié)中,將編寫一個應(yīng)用程序來動態(tài)地計算數(shù)學(xué)表達(dá)式。首先不使用eval()來解決這個問題,那么需要通過以下步驟:

解析輸入的表達(dá)式。將表達(dá)式的組成部分變?yōu)镻ython對象(數(shù)字、運(yùn)算符、函數(shù)等等)。將所有的東西合并成一個表達(dá)式。確認(rèn)該表達(dá)式在Python中是有效的。計算最終表達(dá)式并返回結(jié)果。

考慮到Python可以處理和計算的各種表達(dá)式非常耗時。其實(shí)我們可以使用eval()來解決這個問題,而且通過上文我們已經(jīng)學(xué)會了幾種技術(shù)來規(guī)避相關(guān)的安全風(fēng)險。

首先創(chuàng)建一個新的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())}

"""

在這段代碼中,我們首先導(dǎo)入math模塊。這個模塊使用預(yù)定義的函數(shù)和常數(shù)進(jìn)行數(shù)學(xué)運(yùn)算。常量ALLOWED_NAMES?保存了一個包含數(shù)學(xué)中非特變量名的字典。這樣就可以用eval()來使用它們。

我們還定義了另外三個字符串常量。將使用它們作為腳本的用戶界面,并根據(jù)需要打印到屏幕上。

現(xiàn)在準(zhǔn)備編寫核心功能,首先編寫一個函數(shù),接收數(shù)學(xué)表達(dá)式作為輸入,并返回其結(jié)果。此外還需要寫一個叫做evaluate()的函數(shù),如下所示。

defevaluate(expression):

"""Evaluateamathexpression."""

#編譯表達(dá)式

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

#驗(yàn)證允許名稱

fornameincode.co_names:

ifnamenotinALLOWED_NAMES:

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

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

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

定義了evaluate(),該函數(shù)將字符串表達(dá)式作為參數(shù),并返回一個浮點(diǎn)數(shù),代表將字符串作為數(shù)學(xué)表達(dá)式進(jìn)行計算的結(jié)果。使用compile()將輸入的字符串表達(dá)式變成編譯的Python代碼。如果用戶輸入了一個無效的表達(dá)式,編譯操作將引發(fā)一個SyntaxError。使用一個for循環(huán),檢查表達(dá)式中包含的名字,并確認(rèn)它們可以在最終表達(dá)式中使用。如果用戶提供的名字不在允許的名字列表中,那么會引發(fā)一個NameError。執(zhí)行數(shù)學(xué)表達(dá)式的實(shí)際計算。注意將自定義的字典傳遞給了globals和locals。ALLOWED_NAMES保存了數(shù)學(xué)中定義的函數(shù)和常量。

注意:由于這個應(yīng)用程序使用了math中定義的函數(shù),需要注意,當(dāng)我們用一個無效的輸入值調(diào)用這些函數(shù)時,其中一些函數(shù)將拋出ValueError異常。

例如,math.sqrt(-10)?會引發(fā)一個異常,因?yàn)?10的平方根是未定義的。我們會在稍后的代碼中看到如何捕捉該異常。

為globals和locals參數(shù)使用自定義值,加上名稱檢查,可以將與使用eval()有關(guān)的安全風(fēng)險降到最低。

當(dāng)在main()中編寫其代碼時,數(shù)學(xué)表達(dá)式計算器就完成了。在這個函數(shù)中,定義程序的主循環(huán),結(jié)束讀取和計算用戶在命令行中輸入的表達(dá)式的循環(huán)。

在這個例子中,應(yīng)用程序?qū)ⅲ?/p>

向用戶打印一條歡迎信息顯示一個提示,準(zhǔn)備讀取用戶的輸入提供獲取使用說明和終止應(yīng)用程序的選項(xiàng)讀取用戶的數(shù)學(xué)表達(dá)式計算用戶的數(shù)學(xué)表達(dá)式將計算的結(jié)果打印到屏幕上

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

#對表達(dá)式進(jìn)行計算并處理錯誤

try:

result=evaluate(expression)

exceptSyntaxError:

#如果用戶輸入了一個無效的表達(dá)式

print("Invalidinputexpressionsyntax")

continue

except(NameError,ValueError)aserr:

#如果用戶試圖使用一個不允許的名字

#對于一個給定的數(shù)學(xué)函數(shù)來說是一個無效的值

print(err)

continue

#如果沒有發(fā)生錯誤,則打印結(jié)果

print(f"Theresultis:{result}")

if__name__=="__main__":

main()

在main()?中,首先打印WELCOME消息。然后在一個try語句中讀取用戶的輸入,以捕獲鍵盤中斷和EOFError。如果這些異常發(fā)生,就終止應(yīng)用程序。

如果用戶輸入幫助選項(xiàng),那么應(yīng)用程序就會顯示使用指南。同樣地,如果用戶輸入quit或exit,那么應(yīng)用程序就會終止。

最后,使用evaluate()?來計算用戶的數(shù)學(xué)表達(dá)式,然后將結(jié)果打印到屏幕上。值得注意的是,對evaluate()的調(diào)用會引發(fā)以下異常。

SyntaxError:語法錯誤,當(dāng)用戶輸入一個不符合Python語法的表達(dá)式時,就會發(fā)生這種情況。NameError:當(dāng)用戶試圖使用一個不允許的名稱(函數(shù)、類或?qū)傩裕r,就會發(fā)生這種情況。ValueError:當(dāng)用戶試圖使用一個不允許的值作為數(shù)學(xué)中某個函數(shù)的輸入時,就會發(fā)生這種情況。

注意,在main()中,捕捉了所有已知異常,并相應(yīng)地打印信息給用戶。這將使用戶能夠?qū)彶楸?/p>

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論