高可擴展的任務執行架構需求 一文中 , 山姆鍋提到基於 Python 的超輕量級沙盒 (sandbox)。 沙盒是一個有限制的執行環境 , 用來執行不受信任的程式碼 / 腳本 (script)。 本文山姆鍋基於需要 , 實驗一個 Python 沙盒的開源專案 。

請注意 : 按照該開源專案所述 , 不保證所提供的方法一定安全 。

為什麼需要沙盒環境 ?

有時候應用軟體為了彈性起見 , 會允許使用者自己撰寫腳本來擴充軟體功能 。 由於不能預期使用者撰寫的內容 , 如果沒有適當控制的話 , 小則讓程式崩潰 , 大則出現安全漏洞 。 為了解決這樣的問題 , 有人提出讓不受信任的程式在一個沙盒環境中執行 。 就像小孩子玩沙一樣 , 只要不超出沙盒的範圍 , 基本上不用擔心安全問題 。

但要在 Python ( 精確講是 CPython ) 實作一個安全的沙盒環境並不是那麼簡單 。 使用 Python 內建的 eval 衍生的安全問題 眾所皆知

沙盒環境的需求

為了安全原因 , 沙盒中的程式允許執行的功能是越少越好 。 在 高可擴展的任務執行架構需求 文章所提的任務執行服務 , 其中代理人的邏輯便是由腳本實現 。 代理人的功能相對單純 , 只需要邏輯判斷跟呼叫平台提供的函式完成任務 。 由於複雜跟不安全的操作都由平台提供 , 針對代理人的腳本 , 山姆鍋的需求只有 :

  1. 支援運算式 。
  2. 支援條件式 、 以及有限制的 for 迴圈流程控制 。

除此之外的功能全部都要禁止使用 !

沙盒環境的實作

上面提到使用 Python 的 eval 來執行不受信任的程式是非常危險的行為 。 那有沒有其他較安全的方案 ? 俗話說得好 :「 萬事問谷歌大神讓你變聰明 」( 誤 )。 不管如何 , 山姆鍋找到一個有希望的開源專案 : asteval

「 工欲善其事 , 必先利其器 」, 讓我們進一步來了解 asteval 的工作原理 。

asteval 的運作原理

asteval 的運作的基本原理如下 :

  1. 使用 Python 內建的 ast 模組先將腳本剖析 (parse) 成抽象語法樹 (Abstract Syntax Tree ; AST) 的形式 ,
  2. 尋訪 (visit) 語法樹 , 只執行允許的功能 。

感覺還蠻容易理解 , 對於要求安全性來說這是好事 。 容易理解才比較能看出哪裏有問題 。

使用範例

底下是 asteval 網站提供的範例 :

>>> from asteval import Interpreter
>>> aeval = Interpreter()

>>> aeval('x = sqrt(3)')
>>> aeval('print x')
1.73205080757
>>> aeval('''for i in range(10):
print i, sqrt(i), log(1+1)
''')
0 0.0 0.0
1 1.0 0.69314718056
2 1.41421356237 1.09861228867
3 1.73205080757 1.38629436112
4 2.0 1.60943791243
5 2.2360679775 1.79175946923
6 2.44948974278 1.94591014906
7 2.64575131106 2.07944154168
8 2.82842712475 2.19722457734
9 3.0 2.30258509299

延伸思考

  1. 雖然執行時期一定需要檢查 , 但在腳本儲存到系統之前似乎也可以先做一次大略的檢查 。 這樣在沒有實際執行腳本的情況下 , 就可以過濾掉大部份有問題的程式碼 。
  2. 由於山姆鍋的需求比 asteval 提供的還少 , 除了原先它不允許的用法之外 , 還要進一步做限制 。 例如 :
  • 不支援 while 迴圈 。
  • 迴圈數不允許超過 100。
  • 迴圈中不允許再包含迴圈 。
  • 不支援函式定義 。
  • 不允許 '**' 運算子 。
  1. 因為採用防堵的設計 , 總有可能遇到沒考慮到的情況導致出現漏洞 。

結語

根據 asteval 網站提供的資訊 , 透過它來執行腳本比直接使用 eval 平均慢約 4 倍左右 。 另外 , 這個沙盒環境沒有處理腳本消耗過多資源的問題 , 例如 :'2**1234567890' 這樣簡單的運算式就足夠讓腳本卡住 。

參考資料

Abstract Syntax Tree: https://en.wikipedia.org/wiki/Abstract_syntax_tree

抽象語法樹 。