From b954558e73a92c430c2571fdc9081467ea6ccb40 Mon Sep 17 00:00:00 2001 From: Vyom Mani Tiwari Date: Mon, 8 Jun 2026 11:42:37 +0530 Subject: [PATCH 1/2] RANGER-5629: Harden GraalJS engine configuration used for policy condition (_expression) evaluation --- .../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 6fafa10397..abffccfa5e 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 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 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 createMeth = 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); + createMeth = graalJsCls.getMethod("create", engineCls, ctxBldCls); + } catch (Throwable t) { + LOG.warn("GraalScriptEngineCreator(): failed to initialize", t); + } finally { + this.createMethod = createMeth; + 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 getGraalVmConfigs(Configuration configuration, Map graalVmConfigsDefault) { - LOG.debug("===>> GraalScriptEngineCreator.getGraalVmConfigs()"); - - Map ret = new HashMap<>(); - - // set configs from ranger security config values, if present - for (Map.Entry 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 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 allowAccessMeth = 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())) { + allowAccessMeth.invoke(haBuilder, m); + } } + } catch (ClassNotFoundException e) { + LOG.warn("GraalScriptEngineCreator.buildHostAccess(): class not found: {}", className); } } - - // add default values if not already set - for (Map.Entry 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 findFiles(FilenameFilter filenameFilter) { - String classPath = System.getProperty(CONFIG_JAVA_CLASS_PATH); - List configDirs = new ArrayList<>(5); - Set 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) 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 c84c187811..a19abe88e2 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 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 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 escaped = new ArrayList<>(); + for (Map.Entry 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 userGroups, Set userRoles, List resourceTags) { RangerAccessResource resource = mock(RangerAccessResource.class); From d2c429e0c2076cb7173dbac1489558a2783c275e Mon Sep 17 00:00:00 2001 From: Vyom Mani Tiwari Date: Thu, 11 Jun 2026 09:46:00 +0530 Subject: [PATCH 2/2] RANGER-5629: changed the local variable name to address review comment --- .../ranger/plugin/util/GraalScriptEngineCreator.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 abffccfa5e..983c0ddf76 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 @@ -52,7 +52,7 @@ public class GraalScriptEngineCreator implements ScriptEngineCreator { private final Object ctxBuilder; public GraalScriptEngineCreator() { - Method createMeth = null; + Method createMethod = null; Object builder = null; try { Object hostAccess = buildHostAccess(); @@ -60,11 +60,11 @@ public GraalScriptEngineCreator() { Class engineCls = Class.forName(CLS_ENGINE); Class graalJsCls = Class.forName(CLS_GRAAL_JS_ENGINE); Class ctxBldCls = Class.forName(CLS_CONTEXT_BUILDER); - createMeth = graalJsCls.getMethod("create", engineCls, ctxBldCls); + createMethod = graalJsCls.getMethod("create", engineCls, ctxBldCls); } catch (Throwable t) { LOG.warn("GraalScriptEngineCreator(): failed to initialize", t); } finally { - this.createMethod = createMeth; + this.createMethod = createMethod; this.ctxBuilder = builder; } } @@ -89,12 +89,12 @@ 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 allowAccessMeth = haBuilderCls.getMethod("allowAccess", Executable.class); + 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())) { - allowAccessMeth.invoke(haBuilder, m); + allowAccessMethod.invoke(haBuilder, m); } } } catch (ClassNotFoundException e) {