高可擴展的任務執行架構需求 一文中,山姆鍋提到基於 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 網站提供的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> 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

抽象語法樹。