Activiti中的安全脚本如何工作
最近的Activiti 5.21.0版本的突出特點之一是“安全腳本”。 Activiti用戶指南中詳細介紹了啟用和使用此功能的方法 。 在這篇文章中,我將向您展示我們如何實現其最終實現以及它在幕后所做的事情。 當然,由于這是我通常的簽名風格,因此我們還將對性能進行一些了解。
問題
長期以來,Activiti引擎一直支持腳本任務(和任務/執行偵聽器)的腳本編寫。 所使用的腳本在流程定義中定義,并且可以在部署流程定義后直接執行。 這是很多人喜歡的東西。 這與Java委托類或委托表達式有很大的不同,因為它們通常需要將實際的邏輯放在類路徑上。 它本身已經引入了某種“保護”,因為高級用戶通常只能這樣做。
但是,使用腳本不需要這種“額外步驟”。 如果您將腳本任務的功能提供給最終用戶(并且我們從某些用戶那里知道某些公司確實擁有此用例),那么所有的賭注都將大打折扣。 您可以通過執行流程實例來關閉JVM或執行惡意操作。
第二個問題是編寫一個無限循環且永無止境的腳本非常容易。 第三個問題是,腳本在執行時可以輕松使用大量內存,并占用大量系統資源。
讓我們來看入門的第一個問題。 首先,讓我們添加最新和最大的Activiti引擎依賴性以及內存數據庫庫中的H2:
<dependencies><dependency><groupId>org.activiti</groupId><artifactId>activiti-engine</artifactId><version>5.21.0</version></dependency><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>1.3.176</version></dependency> </dependencies>我們將在這里使用的過程非常簡單:只是一個開始事件,腳本任務和結束。 此處的過程并不是真正的重點,而是腳本執行。
我們將嘗試的第一個腳本有兩件事:它會獲取并顯示我的機器的當前網絡配置(但顯然有此想法的更危險的應用程序), 然后關閉整個JVM 。 當然,在適當的設置中,可以通過確保運行邏輯的用戶在計算機上沒有任何重要權限(但不能解決占用資源的問題)來緩解其中的某些問題。 但是我認為這很好地說明了為什么將腳本的功能提供給幾乎任何人實際上在安全方面是很糟糕的。
<scriptTask id="myScriptTask" scriptFormat="javascript"><script>var s = new java.util.Scanner(java.lang.Runtime.getRuntime().exec("ifconfig").getInputStream()).useDelimiter("\\A");var output = s.hasNext() ? s.next() : "";java.lang.System.out.println("--- output = " + output);java.lang.System.exit(1);</script> </scriptTask>讓我們部署流程定義并執行流程實例:
public class Demo1 {public static void main (String[] args) {// Build engine and deployProcessEngine processEngine = new StandaloneInMemProcessEngineConfiguration().buildProcessEngine();RepositoryService repositoryService = processEngine.getRepositoryService();repositoryService.createDeployment().addClasspathResource("process.bpmn20.xml").deploy();// Start process instanceRuntimeService runtimeService = processEngine.getRuntimeService();runtimeService.startProcessInstanceByKey("myProcess");} }給出以下輸出(此處縮短):
—輸出= eth0鏈接encap:以太網
inet地址:192.168.0.114廣播:192.168.0.255掩碼:255.255.255.0
… 流程以退出代碼1完成
它輸出有關我所有網絡接口的信息,然后關閉整個JVM。 是的。 太恐怖了
嘗試納斯霍恩
第一個問題的解決方案是,我們需要將要在腳本中公開的內容列入白名單,并且默認情況下將所有內容都列入黑名單。 這樣,用戶將無法運行任何可以做惡意事情的類或方法。
在Activiti中,當javascript腳本任務是流程定義的一部分時,我們使用JDK中的ScriptEngine類將此腳本提供給JDK中嵌入的javascript引擎。 在JDK 6/7中是Rhino,在JDK 8中是Nashorn。 首先,我進行了一些認真的搜索,以找到Nashorn的解決方案(因為這將更加適用于未來)。 Nashorn確實具有“類過濾器”概念,可以有效地實施白名單。 但是,ScriptEngine抽象沒有任何工具可以實際調整或配置Nashorn引擎。 我們必須做一些底層的魔術才能使其正常工作。
代替使用默認的Nashorn腳本引擎,我們自己在“ SecureScriptTask”(這是常規的JavaDelegate)中實例化Nashorn腳本引擎。 注意使用jdk.nashorn。*包的用法–不太好。 我們遵循https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/api.html中的文檔,通過在Nashorn引擎中添加“ ClassFilter”來使腳本執行更加安全。 這實際上是可以在腳本中使用的已批準類的白名單。
public class SafeScriptTaskDemo2 implements JavaDelegate {private Expression script;public void execute(DelegateExecution execution) throws Exception {NashornScriptEngineFactory factory = new NashornScriptEngineFactory();ScriptEngine scriptEngine = factory.getScriptEngine(new SafeClassFilter());ScriptingEngines scriptingEngines = Context.getProcessEngineConfiguration().getScriptingEngines();Bindings bindings = scriptingEngines.getScriptBindingsFactory().createBindings(execution, false);scriptEngine.eval((String) script.getValue(execution), bindings);System.out.println("Java delegate done");}public static class SafeClassFilter implements ClassFilter {public boolean exposeToScripts(String s) {return false;}}}當執行時,上面的腳本將不會執行,將引發一個異常,指出“線程“主”中的異常java.lang.RuntimeException:java.lang.ClassNotFoundException:java.lang.System.out.println”。
請注意,ClassFilter僅可從JDK 1.8.0_40使用(相當早!)。
但是,這不能解決無限循環的第二個問題。 讓我們執行一個簡單的腳本:
while (true) {print("Hello"); }您可以猜測會做什么。 這將永遠運行。 如果幸運的話,腳本任務在事務中執行時,事務超時將發生。 但這遠不是一個體面的解決方案,因為它浪費了一段時間的CPU資源而無所作為。
使用大量內存的第三個問題也很容易證明:
var array = [] for(var i = 0; i < 2147483647; ++i) {array.push(i);java.lang.System.out.println(array.length); }啟動流程實例時,內存將快速填滿(僅以幾個MB開頭):
并最終以OutOfMemoryException: 線程“ main”中的異常java.lang.OutOfMemoryError:超出了GC開銷限制
切換到犀牛
在下面的示例與上一個示例之間,花費了大量時間使Nashorn以某種方式攔截或應對無限循環/內存使用情況。 但是,經過大量搜索和試驗后,似乎這些功能在Nashorn中還不是(還?)。 快速搜索將告訴您,我們不是唯一尋求解決方案的人。 通常會提到Rhino確實具有解決此問題的功能。
例如,在JDK <8中,Rhino javascript引擎具有“ instructionCount”回調機制,而Nashorn中不存在該回調機制。 它基本上為您提供了一種在回叫中執行邏輯的方法,該回叫會自動被每x條指令( 字節碼指令!)調用。 我首先嘗試(并且浪費了很多時間)用Nashorn模仿指令計數的想法,例如,首先美化腳本(因為人們可以將整個腳本寫在一行上),然后在腳本中注入一行代碼來觸發回調。 但是,那是1)做起來不是很簡單2)一個人仍然可以在無限運行/使用大量內存的一行上寫一條指令。
搜索被困在那里,導致我們找到了Mozilla的Rhino引擎 。 自從很久以前將其包含在JDK中以來,它實際上就已經進一步發展了,而JDK中的版本并未進行這些更改! 閱讀了(非常稀疏的)Rhino文檔后,很明顯Rhino在我們的用例方面似乎具有更豐富的功能。
Nashorn的ClassFilter與Rhino中的“ ClassShutter”概念匹配。 使用Rhino的回調機制解決了cpu和內存問題:您可以定義一個稱為x指令的回調。 這意味著一行可能是數百個字節代碼指令,并且每x條指令我們都會得到一個回調……。 在執行腳本時,它非常適合監視我們的cpu和內存使用情況。
如果您對我們在代碼中實現這些想法感興趣, 請在此處查看 。
這確實意味著無論您使用什么JDK版本,都不會使用嵌入式javascript引擎,而會一直使用Rhino。
嘗試一下
要使用新的安全腳本功能,請添加以下依賴關系:
<dependency><groupId>org.activiti</groupId><artifactId>activiti-secure-javascript</artifactId><version>5.21.0</version> </dependency>這將暫時包括Rhino引擎。 這還將啟用SecureJavascriptConfigurator ,在創建流程引擎之前需要對其進行配置:
SecureJavascriptConfigurator configurator = new SecureJavascriptConfigurator().setWhiteListedClasses(new HashSet<String>(Arrays.asList("java.util.ArrayList"))).setMaxStackDepth(10).setMaxScriptExecutionTime(3000L).setMaxMemoryUsed(3145728L).setNrOfInstructionsBeforeStateCheckCallback(10);ProcessEngine processEngine = new StandaloneInMemProcessEngineConfiguration().addConfigurator(configurator).buildProcessEngine();這會將安全腳本配置為
- 每10條指令,檢查一次CPU執行時間和內存使用情況
- 給腳本3秒3MB的執行時間
- 將堆棧深度限制為10(以避免遞歸)
- 將數組列表公開為可以在腳本中安全使用的類
從上方運行試圖讀取ifconfig并關閉JVM的腳本會導致:
TypeError:無法在對象[JavaPackage java.lang.Runtime]中調用屬性getRuntime。 它不是功能,而是“對象”。
從上面運行無限循環腳本可以得出
線程“ main” java.lang.Error中的異常:最大variableScope時間超過了3000 ms
從上面運行內存使用腳本可以
線程“主” java.lang.Error中的異常:內存限制達到3145728字節
和歡呼! 解決了上面定義的問題
性能
我做了一個非常不科學的快速檢查……我幾乎不敢分享它,因為結果違背了我的設想。
我創建了一個快速主程序,該主程序運行帶有腳本任務的流程實例10000次:
public class PerformanceUnsecure {public static void main (String[] args) {ProcessEngine processEngine = new StandaloneInMemProcessEngineConfiguration().buildProcessEngine();RepositoryService repositoryService = processEngine.getRepositoryService();repositoryService.createDeployment().addClasspathResource("performance.bpmn20.xml").deploy();Random random = new Random();RuntimeService runtimeService = processEngine.getRuntimeService();int nrOfRuns = 10000;long total = 0;for (int i=0; i<nrOfRuns; i++) {Map<String, Object> variables = new HashMap<String, Object>();variables.put("a", random.nextInt());variables.put("b", random.nextInt());long start = System.currentTimeMillis();runtimeService.startProcessInstanceByKey("myProcess", variables);long end = System.currentTimeMillis();total += (end - start);}System.out.println("Finished process instances : " + processEngine.getHistoryService().createHistoricProcessInstanceQuery().count());System.out.println("Total time = " + total + " ms");System.out.println("Avg time/process instance = " + ((double)total/(double)nrOfRuns) + " ms");}}流程定義只是一個開始->腳本任務->結束。 腳本任務只是將變量添加到變量中,然后將結果保存在第三個變量中。
<scriptTask id="myScriptTask" scriptFormat="javascript"><script>var c = a + b;execution.setVariable('c', c);</script> </scriptTask>我運行了五次,平均每個流程實例為2.57毫秒。 這是在最近的JDK 8(所以是Nashorn)上。
然后,我切換了上面的前兩行以使用新的安全腳本,從而切換到Rhino并啟用了安全功能:
SecureJavascriptConfigurator configurator = new SecureJavascriptConfigurator().addWhiteListedClass("org.activiti.engine.impl.persistence.entity.ExecutionEntity").setMaxStackDepth(10).setMaxScriptExecutionTime(3000L).setMaxMemoryUsed(3145728L).setNrOfInstructionsBeforeStateCheckCallback(1);ProcessEngine processEngine = new StandaloneInMemProcessEngineConfiguration().addConfigurator(configurator).buildProcessEngine();再次進行了五次運行……并獲得了1.07毫秒/流程實例。 這是同一件事的兩倍以上 。
當然,這不是一個真正的考驗。 我以類白名單檢查和回調為前提,假設Rhino的執行速度會變慢,但是沒有這種事情。 也許這種特殊情況更適合Rhino……如果有人可以解釋,請發表評論。 但這仍然是一個有趣的結果。
結論
如果您在流程定義中使用腳本,請仔細閱讀引擎中的此新安全腳本功能。 由于這是一項新功能,非常歡迎反饋和改進!
翻譯自: https://www.javacodegeeks.com/2016/06/secure-scripting-activiti-works.html
總結
以上是生活随笔為你收集整理的Activiti中的安全脚本如何工作的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: oracle线程阻塞_Oracle Se
- 下一篇: 大例外背后的真相