This is an automated email from the ASF dual-hosted git repository.
pradeepagrawal8184 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/master by this push:
new 85b8eca06 RANGER-5629: Harden GraalJS engine configuration used for
policy condition (_expression) evaluation (#1002)
85b8eca06 is described below
commit 85b8eca0693208d435546d372edaf037a04b4c15
Author: Vyom Mani Tiwari <[email protected]>
AuthorDate: Wed Jun 17 17:29:07 2026 +0530
RANGER-5629: Harden GraalJS engine configuration used for policy condition
(_expression) evaluation (#1002)
* RANGER-5629: Harden GraalJS engine configuration used for policy
condition (_expression) evaluation
* RANGER-5629: changed the local variable name to address review comment
---
.../plugin/util/GraalScriptEngineCreator.java | 172 ++++++++-------------
.../RangerRequestScriptEvaluatorTest.java | 60 +++++++
2 files changed, 125 insertions(+), 107 deletions(-)
diff --git
a/agents-common/src/main/java/org/apache/ranger/plugin/util/GraalScriptEngineCreator.java
b/agents-common/src/main/java/org/apache/ranger/plugin/util/GraalScriptEngineCreator.java
index 6fafa1039..983c0ddf7 100644
---
a/agents-common/src/main/java/org/apache/ranger/plugin/util/GraalScriptEngineCreator.java
+++
b/agents-common/src/main/java/org/apache/ranger/plugin/util/GraalScriptEngineCreator.java
@@ -19,72 +19,61 @@
package org.apache.ranger.plugin.util;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.fs.Path;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.script.Bindings;
-import javax.script.ScriptContext;
import javax.script.ScriptEngine;
-import javax.script.ScriptEngineManager;
-import java.io.File;
-import java.io.FilenameFilter;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import static java.util.Objects.requireNonNull;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.function.Predicate;
public class GraalScriptEngineCreator implements ScriptEngineCreator {
private static final Logger LOG =
LoggerFactory.getLogger(GraalScriptEngineCreator.class);
- private static final String ENGINE_NAME = "graal.js";
- private static final String CONFIG_PREDIX_JVM = "polyglot";
- private static final String CONFIG_PREDIX_PLUGIN =
"ranger.plugin.script.";
- private static final String CONFIG_JAVA_CLASS_PATH = "java.class.path";
-
- // instance variables
- private final Map<String, Boolean> graalVmConfigs = new HashMap<>();
+ private static final String ENGINE_NAME = "graal.js";
+ private static final String CLS_HOST_ACCESS =
"org.graalvm.polyglot.HostAccess";
+ private static final String CLS_HOST_ACCESS_BUILDER =
"org.graalvm.polyglot.HostAccess$Builder";
+ private static final String CLS_CONTEXT = "org.graalvm.polyglot.Context";
+ private static final String CLS_CONTEXT_BUILDER =
"org.graalvm.polyglot.Context$Builder";
+ private static final String CLS_ENGINE = "org.graalvm.polyglot.Engine";
+ private static final String CLS_GRAAL_JS_ENGINE =
"com.oracle.truffle.js.scriptengine.GraalJSScriptEngine";
+
+ // Script-API classes whose public instance methods are exported to
scripts.
+ // getDeclaredMethods() returns only methods declared directly in each
class,
+ // NOT inherited ones (e.g. Object.getClass()) — so the reflection chain is
+ // closed without needing @HostAccess.Export annotations on those classes.
+ private static final String[] SCRIPT_API_CLASSES = new String[] {
+
"org.apache.ranger.plugin.policyengine.RangerRequestScriptEvaluator",
+ "org.apache.ranger.plugin.contextenricher.RangerTagForEval"
+ };
+ private final Method createMethod;
+ private final Object ctxBuilder;
- // default constructor
public GraalScriptEngineCreator() {
- Map<String, Boolean> graalVmConfigsDefault = new HashMap<>(4);
//setting smallest size, which is big enough to avoid expand
- Configuration configuration = new Configuration();
- FilenameFilter fileNameFilter = (dir, name) ->
name.startsWith("ranger-") && name.endsWith("security.xml");
-
- graalVmConfigsDefault.put("polyglot.js.allowHostAccess",
Boolean.TRUE); //default is true for backward(Nashorn) compatibility
- graalVmConfigsDefault.put("polyglot.js.nashorn-compat", Boolean.TRUE);
//default is true for backward(Nashorn) compatibility
-
- for (String file : findFiles(fileNameFilter)) {
- configuration.addResource(new Path(file));
+ Method createMethod = null;
+ Object builder = null;
+ try {
+ Object hostAccess = buildHostAccess();
+ builder = buildContextBuilder(hostAccess);
+ Class<?> engineCls = Class.forName(CLS_ENGINE);
+ Class<?> graalJsCls = Class.forName(CLS_GRAAL_JS_ENGINE);
+ Class<?> ctxBldCls = Class.forName(CLS_CONTEXT_BUILDER);
+ createMethod = graalJsCls.getMethod("create", engineCls,
ctxBldCls);
+ } catch (Throwable t) {
+ LOG.warn("GraalScriptEngineCreator(): failed to initialize", t);
+ } finally {
+ this.createMethod = createMethod;
+ this.ctxBuilder = builder;
}
-
- graalVmConfigs.putAll(getGraalVmConfigs(configuration,
graalVmConfigsDefault));
}
public ScriptEngine getScriptEngine(ClassLoader clsLoader) {
ScriptEngine ret = null;
-
- if (clsLoader == null) {
- clsLoader = getDefaultClassLoader();
- }
-
try {
- ScriptEngineManager mgr = new ScriptEngineManager(clsLoader);
-
- ret = mgr.getEngineByName(ENGINE_NAME);
-
- if (ret != null) {
- // enable configured script features
- Bindings bindings =
ret.getBindings(ScriptContext.ENGINE_SCOPE);
- bindings.putAll(graalVmConfigs);
- ret.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
+ if (createMethod != null && ctxBuilder != null) {
+ ret = (ScriptEngine) createMethod.invoke(null, null,
ctxBuilder);
}
} catch (Throwable t) {
LOG.debug("GraalScriptEngineCreator.getScriptEngine(): failed to
create engine type {}", ENGINE_NAME, t);
@@ -96,68 +85,37 @@ public ScriptEngine getScriptEngine(ClassLoader clsLoader) {
return ret;
}
- private Map<String, Boolean> getGraalVmConfigs(Configuration
configuration, Map<String, Boolean> graalVmConfigsDefault) {
- LOG.debug("===>> GraalScriptEngineCreator.getGraalVmConfigs()");
-
- Map<String, Boolean> ret = new HashMap<>();
-
- // set configs from ranger security config values, if present
- for (Map.Entry<String, String> entry :
configuration.getPropsWithPrefix(CONFIG_PREDIX_PLUGIN).entrySet()) {
- String key = entry.getKey();
- String value = entry.getValue();
-
- if (StringUtils.isNotBlank(value)) {
- ret.put(key, Boolean.valueOf(value));
- }
- }
-
- // add JVM options if not already set
- for (Map.Entry<Object, Object> entry :
System.getProperties().entrySet()) {
- if (entry.getKey().toString().startsWith(CONFIG_PREDIX_JVM)) {
- String key = entry.getKey().toString();
- String value = entry.getValue().toString();
-
- if (StringUtils.isNotBlank(value) && ret.get(key) == null) {
- ret.put(key, Boolean.valueOf(value));
+ private Object buildHostAccess() throws Exception {
+ Class<?> hostAccessCls = Class.forName(CLS_HOST_ACCESS);
+ Class<?> haBuilderCls = Class.forName(CLS_HOST_ACCESS_BUILDER);
+ Object haBuilder = hostAccessCls.getMethod("newBuilder").invoke(null);
+ Method allowAccessMethod = haBuilderCls.getMethod("allowAccess",
Executable.class);
+ for (String className : SCRIPT_API_CLASSES) {
+ try {
+ for (Method m : Class.forName(className).getDeclaredMethods())
{
+ if (Modifier.isPublic(m.getModifiers()) &&
!Modifier.isStatic(m.getModifiers())) {
+ allowAccessMethod.invoke(haBuilder, m);
+ }
}
+ } catch (ClassNotFoundException e) {
+ LOG.warn("GraalScriptEngineCreator.buildHostAccess(): class
not found: {}", className);
}
}
-
- // add default values if not already set
- for (Map.Entry<String, Boolean> entry :
graalVmConfigsDefault.entrySet()) {
- String key = entry.getKey();
- Boolean value = entry.getValue();
-
- ret.putIfAbsent(key, value);
- }
-
- LOG.debug("<<=== GraalScriptEngineCreator.getGraalVmConfigs():
ret={}", ret);
-
- return ret;
+ return haBuilderCls.getMethod("build").invoke(haBuilder);
}
- private Set<String> findFiles(FilenameFilter filenameFilter) {
- String classPath = System.getProperty(CONFIG_JAVA_CLASS_PATH);
- List<String> configDirs = new ArrayList<>(5);
- Set<String> ret = new HashSet<>();
-
- for (String path : classPath.split(":")) {
- if (!path.endsWith("jar")) { //ignore jars
- if (path.endsWith("/")) {
- path = path.substring(0, path.length() - 1);
- }
- configDirs.add(path);
- }
- }
-
- for (String configDir : configDirs) {
- File confDir = new File(configDir);
- if (confDir.isDirectory()) {
- for (File file :
requireNonNull(confDir.listFiles(filenameFilter))) {
- ret.add(file.getAbsolutePath());
- }
- }
- }
- return ret;
+ private Object buildContextBuilder(Object hostAccess) throws Exception {
+ Class<?> hostAccessCls = Class.forName(CLS_HOST_ACCESS);
+ Class<?> contextCls = Class.forName(CLS_CONTEXT);
+ Class<?> ctxBuilderCls = Class.forName(CLS_CONTEXT_BUILDER);
+ Object builder = contextCls.getMethod("newBuilder",
String[].class).invoke(null, new Object[] {new String[] {"js"}});
+ builder = ctxBuilderCls.getMethod("allowExperimentalOptions",
boolean.class).invoke(builder, true);
+ builder = ctxBuilderCls.getMethod("allowAllAccess",
boolean.class).invoke(builder, false);
+ builder = ctxBuilderCls.getMethod("allowHostAccess",
hostAccessCls).invoke(builder, hostAccess);
+ builder = ctxBuilderCls.getMethod("allowHostClassLookup",
Predicate.class).invoke(builder, (Predicate<String>) s -> false);
+ builder = ctxBuilderCls.getMethod("allowHostClassLoading",
boolean.class).invoke(builder, false);
+ builder = ctxBuilderCls.getMethod("allowCreateThread",
boolean.class).invoke(builder, false);
+ builder = ctxBuilderCls.getMethod("allowNativeAccess",
boolean.class).invoke(builder, false);
+ return builder;
}
}
diff --git
a/agents-common/src/test/java/org/apache/ranger/plugin/conditionevaluator/RangerRequestScriptEvaluatorTest.java
b/agents-common/src/test/java/org/apache/ranger/plugin/conditionevaluator/RangerRequestScriptEvaluatorTest.java
index c84c18781..a19abe88e 100644
---
a/agents-common/src/test/java/org/apache/ranger/plugin/conditionevaluator/RangerRequestScriptEvaluatorTest.java
+++
b/agents-common/src/test/java/org/apache/ranger/plugin/conditionevaluator/RangerRequestScriptEvaluatorTest.java
@@ -39,6 +39,7 @@
import javax.script.ScriptEngineManager;
import java.io.File;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -46,6 +47,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Function;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -555,6 +557,64 @@ public void testGetAllTagTypes() {
Assertions.assertEquals(new HashSet<>(Arrays.asList("PCI", "PII")),
evaluator.evaluateScript("ctx.getAllTagTypes()"));
}
+ @Test
+ void testBlockJavaClassReferencesWithDefaultConfig() {
+ RangerRequestScriptEvaluator evaluator = createEvaluator();
+ long nonce = System.nanoTime();
+
+ String mJavaType = tempFilePath("javatype", nonce);
+ String mPackages = tempFilePath("packages", nonce);
+ String mCtxRef = tempFilePath("ctxref", nonce);
+ String mRetRef = tempFilePath("retref", nonce);
+ String mTagRef = tempFilePath("tagref", nonce);
+ String mTagAttr = tempFilePath("tagattr", nonce);
+ String mNewFile = tempFilePath("newfile", nonce);
+
+ Function<String, String> reflectExec = marker ->
+ "var Str =
OBJ.getClass().getClassLoader().loadClass('java.lang.String');"
+ + "var rtClz =
OBJ.getClass().getClassLoader().loadClass('java.lang.Runtime');"
+ + "var rt =
rtClz.getMethod('getRuntime').invoke(null);"
+ + "rtClz.getMethod('exec', Str).invoke(rt, 'touch " +
marker + "').waitFor();";
+
+ Map<String, String[]> vectors = new HashMap<>();
+ vectors.put("Java.type('java.lang.Runtime').exec()", new String[] {
+ "var p =
Java.type('java.lang.Runtime').getRuntime().exec('touch " + mJavaType + "');
p.waitFor();", mJavaType});
+ vectors.put("java.lang.Runtime (Packages namespace)", new String[] {
+ "var p = java.lang.Runtime.getRuntime().exec('touch " +
mPackages + "'); p.waitFor();", mPackages});
+ vectors.put("reflection off ctx", new String[] {
+ reflectExec.apply(mCtxRef).replace("OBJ", "ctx"), mCtxRef});
+ vectors.put("reflection off returned ctx.getCurrentTag()", new
String[] {
+ reflectExec.apply(mRetRef).replace("OBJ",
"ctx.getCurrentTag()"), mRetRef});
+ vectors.put("reflection off bound 'tag'", new String[] {
+ reflectExec.apply(mTagRef).replace("OBJ", "tag"), mTagRef});
+ vectors.put("reflection off bound 'tagAttr'", new String[] {
+ reflectExec.apply(mTagAttr).replace("OBJ", "tagAttr"),
mTagAttr});
+ vectors.put("constructor new java.io.File().createNewFile()", new
String[] {
+ "var f = new java.io.File('" + mNewFile + "');
f.createNewFile();", mNewFile});
+
+ List<String> escaped = new ArrayList<>();
+ for (Map.Entry<String, String[]> e : vectors.entrySet()) {
+ File m = new File(e.getValue()[1]);
+ m.delete(); // Just to make sure file does not exist before the
script runs. This is a clean-state guarantee
+ evaluator.evaluateScript(e.getValue()[0]); // returns null when
blocked; never rethrows
+ if (m.exists()) { // The file existing after eval is the ground
truth that the OS command actually ran
+ escaped.add(e.getKey() + " -> created " + m);
+ m.delete();
+ }
+ }
+ Assertions.assertTrue(escaped.isEmpty(), "Sandbox escape OS command
executed via:\n " + String.join("\n ", escaped));
+ }
+
+ private RangerRequestScriptEvaluator createEvaluator() {
+ RangerAccessRequest request = createRequest("test-user",
Collections.emptySet(), Collections.emptySet(),
+ Collections.singletonList(new RangerTag("PII",
Collections.singletonMap("attr1", "v1"))));
+ return new RangerRequestScriptEvaluator(request, scriptEngine, false);
+ }
+
+ private static String tempFilePath(String tag, long nonce) {
+ return new File(System.getProperty("java.io.tmpdir"), "ranger_test_" +
tag + "_" + nonce).getAbsolutePath();
+ }
+
RangerAccessRequest createRequest(String userName, Set<String> userGroups,
Set<String> userRoles, List<RangerTag> resourceTags) {
RangerAccessResource resource = mock(RangerAccessResource.class);