activiti脚本任务_Activiti中的安全脚本如何工作
activiti腳本任務(wù)
最近的Activiti 5.21.0版本的突出特點之一是“安全腳本”。 Activiti用戶指南中詳細介紹了啟用和使用此功能的方法 。 在這篇文章中,我將向您展示我們?nèi)绾螌崿F(xiàn)其最終實現(xiàn)以及它在幕后所做的事情。 當(dāng)然,因為這是我通常的簽名風(fēng)格,所以我們還將對性能進行一些了解。
問題
長期以來,Activiti引擎一直支持腳本任務(wù)(和任務(wù)/執(zhí)行偵聽器)的腳本編寫。 所使用的腳本在流程定義中定義,并且可以在部署流程定義后直接執(zhí)行。 這是很多人喜歡的東西。 這與Java委托類或委托表達式有很大的不同,因為它們通常需要將實際的邏輯放在類路徑上。 它本身已經(jīng)引入了某種“保護”,因為高級用戶通常只能這樣做。
但是,使用腳本不需要這種“額外步驟”。 如果您將腳本任務(wù)的功能提供給最終用戶(并且我們從某些用戶那里知道某些公司確實擁有此用例),那么所有的賭注都將大打折扣。 您可以通過執(zhí)行流程實例來關(guān)閉JVM或執(zhí)行惡意操作。
第二個問題是編寫一個無限循環(huán)且永無止境的腳本非常容易。 第三個問題是,腳本在執(zhí)行時可以輕松使用大量內(nèi)存,并占用大量系統(tǒng)資源。
讓我們來看入門的第一個問題。 首先,讓我們添加最新和最大的Activiti引擎依賴性以及內(nèi)存數(shù)據(jù)庫庫中的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>我們將在這里使用的過程非常簡單:只是一個開始事件,腳本任務(wù)和結(jié)束。 此處的過程并不是真正的重點,而是腳本執(zhí)行。
我們將嘗試的第一個腳本有兩件事:它將獲取并顯示我的機器的當(dāng)前網(wǎng)絡(luò)配置(但顯然有此想法的更危險的應(yīng)用程序), 然后關(guān)閉整個JVM 。 當(dāng)然,在適當(dāng)?shù)脑O(shè)置中,可以通過確保運行邏輯的用戶在計算機上沒有任何重要權(quán)限(但不能解決占用資源的問題)來緩解其中的某些問題。 但是我認為這很好地說明了為什么向幾乎任何人提供腳本的功能在安全性方面確實很糟糕。
<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>讓我們部署流程定義并執(zhí)行流程實例:
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:以太網(wǎng)
inet地址:192.168.0.114廣播:192.168.0.255掩碼:255.255.255.0
… 流程以退出代碼1完成
它輸出有關(guān)我所有網(wǎng)絡(luò)接口的信息,然后關(guān)閉整個JVM。 是的。 太恐怖了
嘗試納斯霍恩
第一個問題的解決方案是,我們需要將要在腳本中公開的內(nèi)容列入白名單,并且默認情況下將所有內(nèi)容都列入黑名單。 這樣,用戶將無法運行任何可以做惡意事情的類或方法。
在Activiti中,當(dāng)javascript腳本任務(wù)是流程定義的一部分時,我們使用JDK中的ScriptEngine類將此腳本提供給JDK中嵌入的javascript引擎。 在JDK 6/7中是Rhino,在JDK 8中是Nashorn。 我首先進行了認真的搜索,以找到Nashorn的解決方案(因為這將更加適用于未來)。 Nashorn確實具有“類過濾器”概念,可以有效地實施白名單。 但是,ScriptEngine抽象沒有任何工具可以實際調(diào)整或配置Nashorn引擎。 我們必須做一些底層的魔術(shù)才能使其正常工作。
代替使用默認的Nashorn腳本引擎,我們自己在“ SecureScriptTask”(這是常規(guī)的JavaDelegate)中實例化Nashorn腳本引擎。 注意使用jdk.nashorn。*包的用法–不太好。 我們遵循h(huán)ttps://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/api.html中的文檔,通過在Nashorn引擎中添加“ ClassFilter”來使腳本執(zhí)行更加安全。 這實際上是可以在腳本中使用的已批準類的白名單。
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;}}}當(dāng)執(zhí)行時,上面的腳本將不會執(zhí)行,將引發(fā)一個異常,指出“主線程”中的異常java.lang.RuntimeException:java.lang.ClassNotFoundException:java.lang.System.out.println”。
請注意,ClassFilter僅可從JDK 1.8.0_40獲得(相當(dāng)近期!)。
但是,這不能解決無限循環(huán)的第二個問題。 讓我們執(zhí)行一個簡單的腳本:
while (true) {print("Hello"); }您可以猜測會做什么。 這將永遠運行。 如果幸運的話,在腳本任務(wù)在事務(wù)中執(zhí)行時,事務(wù)超時將發(fā)生。 但這遠不是一個體面的解決方案,因為它浪費了CPU資源一段時間。
使用大量內(nèi)存的第三個問題也很容易證明:
var array = [] for(var i = 0; i < 2147483647; ++i) {array.push(i);java.lang.System.out.println(array.length); }啟動流程實例時,內(nèi)存將快速填滿(僅以幾個MB開頭):
并最終以O(shè)utOfMemoryException: 線程“ main” java.lang.OutOfMemoryError:超出GC開銷限制結(jié)束
切換到犀牛
在以下示例與上一個示例之間,花費了大量時間使Nashorn以某種方式攔截或應(yīng)對無限循環(huán)/內(nèi)存使用情況。 但是,經(jīng)過大量搜索和試驗后,似乎這些功能在Nashorn中還不是(還?)。 快速搜索將告訴您,我們不是唯一尋求解決方案的人。 通常,人們提到Rhino確實具有解決此問題的功能。
例如,在JDK <8中,Rhino javascript引擎具有'instructionCount'回調(diào)機制,Nashorn中不存在。 它基本上為您提供了一種在回叫中執(zhí)行邏輯的方法,該回叫被每x條指令( 字節(jié)碼指令!)自動調(diào)用。 我首先嘗試(并且浪費了很多時間)用Nashorn模仿指令計數(shù)的想法,例如,首先美化腳本(因為人們可以將整個腳本寫在一行上),然后在腳本中注入一行代碼來觸發(fā)回調(diào)。 但是,那是1)做起來不是很簡單2)一個人仍然可以在無限運行/使用大量內(nèi)存的一行上寫一條指令。
搜索被困在那里,導(dǎo)致我們找到了Mozilla的Rhino引擎 。 自從很久以前就將其包含在JDK中以來,它實際上已經(jīng)進一步發(fā)展了,而JDK中的版本并未進行這些更改! 閱讀了(相當(dāng)稀疏的)Rhino文檔后,很明顯Rhino在我們的用例方面似乎具有更豐富的功能。
Nashorn的ClassFilter與Rhino中的“ ClassShutter”概念匹配。 使用Rhino的回調(diào)機制解決了cpu和內(nèi)存問題:您可以定義一個稱為x指令的回調(diào)。 這意味著一行可能是數(shù)百個字節(jié)代碼指令,并且每x條指令我們都會得到一個回調(diào)……。 在執(zhí)行腳本時,它非常適合監(jiān)視我們的cpu和內(nèi)存使用情況。
如果您對我們在代碼中實現(xiàn)這些想法感興趣, 請在此處查看 。
這確實意味著無論您使用什么JDK版本,都不會使用嵌入式j(luò)avascript引擎,而會一直使用Rhino。
嘗試一下
要使用新的安全腳本功能,請?zhí)砑右韵乱蕾囮P(guān)系:
<dependency><groupId>org.activiti</groupId><artifactId>activiti-secure-javascript</artifactId><version>5.21.0</version> </dependency>這將暫時包括Rhino引擎。 這還將啟用SecureJavascriptConfigurator ,在創(chuàng)建流程引擎之前需要對其進行配置:
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執(zhí)行時間和內(nèi)存使用情況
- 給腳本3秒3MB的執(zhí)行時間
- 將堆棧深度限制為10(以避免遞歸)
- 將數(shù)組列表公開為可以在腳本中安全使用的類
從上方運行試圖讀取ifconfig并關(guān)閉JVM的腳本會導(dǎo)致:
TypeError:無法在對象[JavaPackage java.lang.Runtime]中調(diào)用屬性getRuntime。 它不是功能,而是“對象”。
從上面運行無限循環(huán)腳本可以得到
線程“主” java.lang.Error中的異常:最大variableScope時間超過了3000 ms
從上面運行內(nèi)存使用腳本可以
線程“主” java.lang.Error中的異常:內(nèi)存限制達到3145728字節(jié)
和歡呼! 解決了上面定義的問題
性能
我做了一個非常不科學(xué)的快速檢查……我?guī)缀醪桓曳窒硭?#xff0c;因為結(jié)果違背了我的設(shè)想。
我創(chuàng)建了一個快速主程序,該主程序運行帶有腳本任務(wù)的流程實例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");}}流程定義只是一個開始->腳本任務(wù)->結(jié)束。 腳本任務(wù)只是將變量添加到變量中,然后將結(jié)果保存在第三個變量中。
<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毫秒/流程實例。 這是同一件事的兩倍以上 。
當(dāng)然,這不是一個真正的考驗。 我以類白名單檢查和回調(diào)為前提,假設(shè)Rhino的執(zhí)行速度會變慢,但是沒有這種事情。 也許這種特殊情況更適合Rhino……如果有人可以解釋,請發(fā)表評論。 但這仍然是一個有趣的結(jié)果。
結(jié)論
如果您在流程定義中使用腳本,請仔細閱讀引擎中的此新安全腳本功能。 由于這是一項新功能,非常歡迎反饋和改進!
翻譯自: https://www.javacodegeeks.com/2016/06/secure-scripting-activiti-works.html
activiti腳本任務(wù)
總結(jié)
以上是生活随笔為你收集整理的activiti脚本任务_Activiti中的安全脚本如何工作的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: kn95需要备案吗(KN95备案)
- 下一篇: plsql例外_大例外背后的真相