Root cause
Injection & unsafe execution path exposed
Migrate JavaScript evaluator from Nashorn to GraalJS
How AI contributed
Causal contributionOrkes Conductor 3.21.21 before 3.30.2 contains an unauthenticated remote code execution vulnerability that allows remote attackers to execute arbitrary OS commands by submitting inline workflow definitions containing malicious JavaScript or Python expressions to the workflow API endpoint prior to authentication. Attackers can exploit unsandboxed GraalVM evaluators configured with HostAccess.ALL or allowAllAccess(true) through INLINE, LAMBDA, DO_WHILE, and SWITCH task types to invoke arbitrary system commands via Java reflection or direct subprocess calls.
The AI migration from Nashorn to GraalVM built the JavaScript context with allowHostAccess(HostAccess.ALL) and switched the Python evaluator to allowAllAccess(true), so unauthenticated expressions in INLINE, LAMBDA, DO_WHILE, and SWITCH tasks could reflect into Runtime and ProcessBuilder. The fix denies Class, ClassLoader, Method, Field, Constructor, Array, Runtime, ProcessBuilder, Process, System, Thread, and ThreadGroup.
Root cause
Migrate JavaScript evaluator from Nashorn to GraalJS
Fix
Deny access to some classes in js evaluator, deny host access in python evaluator (#1057)
@@ -21,8 +21,6 @@ import java.util.Map; import java.util.Optional; -import javax.script.ScriptException;- import org.apache.commons.lang3.StringUtils; import com.netflix.conductor.common.metadata.tasks.TaskDef;@@ -192,7 +190,7 @@ private void validateScriptExpression( String expression, Map<String, Object> inputParameters) { try { Object returnValue = ScriptEvaluator.eval(expression, inputParameters);- } catch (ScriptException e) {+ } catch (Exception e) { throw new IllegalArgumentException( String.format("Expression is not well formatted: %s", e.getMessage())); }@@ -17,8 +17,6 @@ import java.util.List; import java.util.Map; -import javax.script.ScriptException;- import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory;@@ -136,10 +134,10 @@ String getEvaluatedCaseValue(WorkflowTask workflowTask, Map<String, Object> task if (StringUtils.isNotBlank(expression)) { LOGGER.debug("Case being evaluated using decision expression: {}", expression); try {- // Evaluate the expression by using the Nashhorn based script evaluator+ // Evaluate the expression by using the GraalJS based script evaluator Object returnValue = ScriptEvaluator.eval(expression, taskInput); caseValue = (returnValue == null) ? "null" : returnValue.toString();- } catch (ScriptException e) {+ } catch (Exception e) { String errorMsg = String.format("Error while evaluating script: %s", expression); LOGGER.error(errorMsg, e); throw new TerminateWorkflowException(errorMsg);@@ -1,5 +1,5 @@ /*- * Copyright 2022 Conductor Authors.+ * Copyright 2025 Conductor Authors. * <p> * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at@@ -12,32 +12,49 @@ */ package com.netflix.conductor.core.execution.evaluators; -import javax.script.ScriptException;+import java.util.HashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import com.netflix.conductor.common.config.ObjectMapperProvider; import com.netflix.conductor.core.events.ScriptEvaluator;-import com.netflix.conductor.core.exception.TerminateWorkflowException;++import com.fasterxml.jackson.core.type.TypeReference;+import com.fasterxml.jackson.databind.ObjectMapper; @Component(JavascriptEvaluator.NAME) public class JavascriptEvaluator implements Evaluator { public static final String NAME = "javascript"; private static final Logger LOGGER = LoggerFactory.getLogger(JavascriptEvaluator.class);+ private final ObjectMapper objectMapper = new ObjectMapperProvider().getObjectMapper(); @Override public Object evaluate(String expression, Object input) { LOGGER.debug("Javascript evaluator -- expression: {}", expression);++ Object inputCopy = new HashMap<>();+ // We make a deep copy because there is a way to make it error out otherwise:+ // e.g. there's an input parameter (an empty map) 'myParam',+ // and an expression which has `$.myParam = {"a":"b"}`; It will put a 'PolyglotMap' from+ // GraalVM into input map+ // and that PolyglotMap can't be evaluated because the context is already closed.+ // this caused a workflow with INLINE task to be undecideable due to Exception in+ // TaskModelProtoMapper+ // on 'to.setInputData(convertToJsonMap(from.getInputData()))' call try {- // Evaluate the expression by using the Javascript evaluation engine.- Object result = ScriptEvaluator.eval(expression, input);- LOGGER.debug("Javascript evaluator -- result: {}", result);- return result;- } catch (ScriptException e) {- LOGGER.error("Error while evaluating script: {}", expression, e);- throw new TerminateWorkflowException(e.getMessage());+ inputCopy =+ objectMapper.readValue(+ objectMapper.writeValueAsString(input), new TypeReference<>() {});+ } catch (Exception e) {+ LOGGER.error("Error making a deep copy of input: {}", expression, e); }++ // Evaluate the expression by using the GraalJS evaluation engine.+ Object result = ScriptEvaluator.eval(expression, inputCopy);+ LOGGER.debug("Javascript evaluator -- result: {}", result);+ return result; } }@@ -29,7 +29,7 @@ public class PythonEvaluator implements Evaluator { @Override public Object evaluate(String expression, Object input) {- try (Context context = Context.newBuilder("python").allowAllAccess(true).build()) {+ try (Context context = Context.newBuilder("python").build()) { if (input instanceof Map) { Map<String, Object> inputMap = (Map<String, Object>) input; @@ -114,8 +114,23 @@ private static void ensureInitialized() { } private static Context createNewContext() {+ HostAccess hostAccess =+ HostAccess.newBuilder(HostAccess.ALL)+ .denyAccess(Class.class)+ .denyAccess(ClassLoader.class)+ .denyAccess(java.lang.reflect.Method.class)+ .denyAccess(java.lang.reflect.Field.class)+ .denyAccess(java.lang.reflect.Constructor.class)+ .denyAccess(java.lang.reflect.Array.class)+ .denyAccess(Runtime.class)+ .denyAccess(ProcessBuilder.class)+ .denyAccess(Process.class)+ .denyAccess(System.class)+ .denyAccess(Thread.class)+ .denyAccess(ThreadGroup.class)+ .build(); return Context.newBuilder("js")- .allowHostAccess(HostAccess.ALL)+ .allowHostAccess(hostAccess) .option("engine.WarnInterpreterOnly", "false") .build(); }@@ -30,6 +30,29 @@ public class InlineTest { + private static final String RCE_EXPRESSION =+ "var ck = $.getClass(); var classClass = ck.getClass();"+ + "var stringClass = classClass.getMethod('getName').getReturnType();"+ + "var forName = classClass.getMethod('forName', stringClass);"+ + "var lookup = function(n){return forName.invoke(null,[n]);};"+ + "var rtClass = lookup('java.lang.Runtime');"+ + "var rt = rtClass.getMethod('getRuntime').invoke(null, []);"+ + "var integerCls = lookup('java.lang.Integer');"+ + "var intType = integerCls.getField('TYPE').get(null);"+ + "var arrayCls = lookup('java.lang.reflect.Array');"+ + "var newInst = arrayCls.getMethod('newInstance', classClass, intType);"+ + "var strArr = newInst.invoke(null, [stringClass, 3]);"+ + "var setM = arrayCls.getMethod('set', lookup('java.lang.Object'), intType, lookup('java.lang.Object'));"+ + "setM.invoke(null,[strArr,0,'sh']); setM.invoke(null,[strArr,1,'-c']); setM.invoke(null,[strArr,2,'id']);"+ + "var arrCls = strArr.getClass();"+ + "var execM = rtClass.getMethod('exec', arrCls);"+ + "var proc = execM.invoke(rt, [strArr]); proc.waitFor();"+ + "var isCl = lookup('java.io.InputStream'); var rdrCl = lookup('java.io.Reader');"+ + "var isrCl = lookup('java.io.InputStreamReader'); var brCl = lookup('java.io.BufferedReader');"+ + "var isr = isrCl.getConstructor(isCl).newInstance(proc.getInputStream());"+ + "var br = brCl.getConstructor(rdrCl).newInstance(isr);"+ + "var out='', line; while((line=br.readLine())!==null) out+=line+'\\n'; out";+ private final WorkflowModel workflow = new WorkflowModel(); private final WorkflowExecutor executor = mock(WorkflowExecutor.class); @@ -173,6 +196,36 @@ public void testInlineDefaultEvaluatorType() { assertEquals(198, ((Map<String, Object>) task.getOutputData().get("result")).get("result")); } + @Test+ public void testRCEExpressionBlockedForJavascript() {+ Inline inline = new Inline(getStringEvaluatorMap());++ Map<String, Object> inputObj = new HashMap<>();+ inputObj.put("evaluatorType", "javascript");+ inputObj.put("expression", RCE_EXPRESSION);++ TaskModel task = new TaskModel();+ task.getInputData().putAll(inputObj);++ inline.execute(workflow, task, executor);+ assertEquals(TaskModel.Status.FAILED_WITH_TERMINAL_ERROR, task.getStatus());+ }++ @Test+ public void testRCEExpressionBlockedForGraalJS() {+ Inline inline = new Inline(getStringEvaluatorMap());++ Map<String, Object> inputObj = new HashMap<>();+ inputObj.put("evaluatorType", "graaljs");+ inputObj.put("expression", RCE_EXPRESSION);++ TaskModel task = new TaskModel();+ task.getInputData().putAll(inputObj);++ inline.execute(workflow, task, executor);+ assertEquals(TaskModel.Status.FAILED_WITH_TERMINAL_ERROR, task.getStatus());+ }+ private Map<String, Evaluator> getStringEvaluatorMap() { Map<String, Evaluator> evaluators = new HashMap<>(); evaluators.put(ValueParamEvaluator.NAME, new ValueParamEvaluator());AI-assisted change 7ea22b3736199ab39e64aaa2fe1f6c9b644b8dacadccb8662bf89d4b9728afb0 · Fix 0ce79b2320c3911c59bd2e4262a85bd9611e691baed15f56a77f1ef3bafeb9dd
Advisory references