This is an automated email from the ASF dual-hosted git repository.

vyommani 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 b7f823187 RANGER-5818: Use the common hardened script engine for 
nestedstructure row-filter evaluation (#1283)
b7f823187 is described below

commit b7f823187f09711a45ce33db609105460b75f3c5
Author: Vyom Mani Tiwari <[email protected]>
AuthorDate: Mon Oct 5 17:07:28 2026 +0530

    RANGER-5818: Use the common hardened script engine for nestedstructure 
row-filter evaluation (#1283)
---
 .../policyengine/RangerRequestScriptEvaluator.java |   3 +-
 .../plugin/util/GraalScriptEngineCreator.java      |  48 ++++++++-
 .../ranger/plugin/util/ScriptEngineUtil.java       |   6 ++
 plugin-nestedstructure/pom.xml                     |   6 --
 .../authorizer/RecordFilterJavaScript.java         | 109 ++++-----------------
 .../authorizer/TestRecordFilterJavaScript.java     |  21 ++--
 6 files changed, 81 insertions(+), 112 deletions(-)

diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerRequestScriptEvaluator.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerRequestScriptEvaluator.java
index 437b89751..7dc7fc30e 100644
--- 
a/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerRequestScriptEvaluator.java
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/policyengine/RangerRequestScriptEvaluator.java
@@ -32,6 +32,7 @@
 import org.apache.ranger.plugin.util.RangerPerfTracer;
 import org.apache.ranger.plugin.util.RangerTimeRangeChecker;
 import org.apache.ranger.plugin.util.RangerUserStore;
+import org.apache.ranger.plugin.util.ScriptEngineUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -160,7 +161,7 @@ public final class RangerRequestScriptEvaluator {
     private static final String  DEFAULT_RANGER_TAG_ATTRIBUTE_DATE_FORMAT     
= "yyyy/MM/dd";
     private static final String  DEFAULT_ATLAS_TAG_ATTRIBUTE_DATE_FORMAT_NAME 
= "ATLAS_DATE_FORMAT";
     private static final String  DEFAULT_ATLAS_TAG_ATTRIBUTE_DATE_FORMAT      
= "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
-    private static final String  SCRIPT_SAFE_PREEXEC                          
= 
"Object.defineProperty(this,'engine',{value:null,writable:false});Object.defineProperty(this,'context',{value:null,writable:false});Object.defineProperty(this,'__noSuchProperty__',{value:null,writable:false});Object.defineProperty(this,'loadWithNewGlobal',{value:null,writable:false});exit=null;quit=null;";
+    private static final String  SCRIPT_SAFE_PREEXEC                          
= ScriptEngineUtil.SCRIPT_SAFE_PREEXEC;
     private static final String  SCRIPT_PREEXEC                               
= SCRIPT_VAR__CTX + "=JSON.parse(" + SCRIPT_VAR__CTX_JSON + "); 
J=JSON.stringify;" +
             SCRIPT_VAR_REQ + "=" + SCRIPT_VAR__CTX + "." + 
SCRIPT_FIELD_REQUEST + ";" +
             SCRIPT_VAR_RES + "=" + SCRIPT_VAR_REQ + "." + 
SCRIPT_FIELD_RESOURCE + ";" +
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 983c0ddf7..781bc6bf1 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
@@ -56,7 +56,7 @@ public GraalScriptEngineCreator() {
         Object builder = null;
         try {
             Object hostAccess = buildHostAccess();
-            builder = buildContextBuilder(hostAccess);
+            builder = newContextBuilder(hostAccess);
             Class<?> engineCls = Class.forName(CLS_ENGINE);
             Class<?> graalJsCls = Class.forName(CLS_GRAAL_JS_ENGINE);
             Class<?> ctxBldCls = Class.forName(CLS_CONTEXT_BUILDER);
@@ -85,6 +85,50 @@ public ScriptEngine getScriptEngine(ClassLoader clsLoader) {
         return ret;
     }
 
+    /**
+     * Graal.js engine that exposes no host classes and no host methods.
+     * A new context builder is used for each engine. Returns null when 
Graal.js cannot be created.
+     */
+    public static ScriptEngine createNoHostAccessScriptEngine() {
+        ScriptEngine ret = null;
+
+        try {
+            Class<?> hostAccessCls = Class.forName(CLS_HOST_ACCESS);
+            Object   hostAccess    = hostAccessCls.getField("NONE").get(null);
+            Object   builder       = newContextBuilder(hostAccess);
+            Class<?> engineCls     = Class.forName(CLS_ENGINE);
+            Class<?> graalJsCls    = Class.forName(CLS_GRAAL_JS_ENGINE);
+            Class<?> ctxBuilderCls = Class.forName(CLS_CONTEXT_BUILDER);
+            Method   createMethod  = graalJsCls.getMethod("create", engineCls, 
ctxBuilderCls);
+
+            ret = (ScriptEngine) createMethod.invoke(null, null, builder);
+        } catch (Throwable t) {
+            
LOG.debug("GraalScriptEngineCreator.createNoHostAccessScriptEngine(): failed to 
create engine type {}", ENGINE_NAME, t);
+        }
+
+        return ret;
+    }
+
+    /**
+     * Closes the polyglot engine behind an engine from {@link 
#createNoHostAccessScriptEngine()},
+     * including every context created from it.
+     */
+    public static void closeScriptEngine(ScriptEngine engine) {
+        if (engine == null) {
+            return;
+        }
+
+        try {
+            Object polyglotEngine = 
engine.getClass().getMethod("getPolyglotEngine").invoke(engine);
+
+            if (polyglotEngine instanceof AutoCloseable) {
+                ((AutoCloseable) polyglotEngine).close();
+            }
+        } catch (Throwable t) {
+            LOG.debug("GraalScriptEngineCreator.closeScriptEngine(): failed to 
close engine", t);
+        }
+    }
+
     private Object buildHostAccess() throws Exception {
         Class<?> hostAccessCls = Class.forName(CLS_HOST_ACCESS);
         Class<?> haBuilderCls = Class.forName(CLS_HOST_ACCESS_BUILDER);
@@ -104,7 +148,7 @@ private Object buildHostAccess() throws Exception {
         return haBuilderCls.getMethod("build").invoke(haBuilder);
     }
 
-    private Object buildContextBuilder(Object hostAccess) throws Exception {
+    private static Object newContextBuilder(Object hostAccess) throws 
Exception {
         Class<?> hostAccessCls = Class.forName(CLS_HOST_ACCESS);
         Class<?> contextCls = Class.forName(CLS_CONTEXT);
         Class<?> ctxBuilderCls = Class.forName(CLS_CONTEXT_BUILDER);
diff --git 
a/agents-common/src/main/java/org/apache/ranger/plugin/util/ScriptEngineUtil.java
 
b/agents-common/src/main/java/org/apache/ranger/plugin/util/ScriptEngineUtil.java
index 424389257..884c17886 100644
--- 
a/agents-common/src/main/java/org/apache/ranger/plugin/util/ScriptEngineUtil.java
+++ 
b/agents-common/src/main/java/org/apache/ranger/plugin/util/ScriptEngineUtil.java
@@ -28,6 +28,12 @@
 public class ScriptEngineUtil {
     private static final Logger LOG = 
LoggerFactory.getLogger(ScriptEngineUtil.class);
 
+    /**
+     * Evaluated before a policy-condition or row-filter script, on the same 
bindings as that script.
+     * Supplements the engine from {@link GraalScriptEngineCreator}.
+     */
+    public static final String SCRIPT_SAFE_PREEXEC = 
"Object.defineProperty(this,'engine',{value:null,writable:false});Object.defineProperty(this,'context',{value:null,writable:false});Object.defineProperty(this,'__noSuchProperty__',{value:null,writable:false});Object.defineProperty(this,'loadWithNewGlobal',{value:null,writable:false});exit=null;quit=null;";
+
     private static final String   SCRIPT_ENGINE_CREATOR_GRAAL    = 
"org.apache.ranger.plugin.util.GraalScriptEngineCreator";
     private static final String   SCRIPT_ENGINE_CREATOR_JS       = 
"org.apache.ranger.plugin.util.JavaScriptEngineCreator";
     private static final String[] SCRIPT_ENGINE_CREATORS         = new 
String[] {SCRIPT_ENGINE_CREATOR_GRAAL, SCRIPT_ENGINE_CREATOR_JS};
diff --git a/plugin-nestedstructure/pom.xml b/plugin-nestedstructure/pom.xml
index 87d7584dc..1abd7216f 100644
--- a/plugin-nestedstructure/pom.xml
+++ b/plugin-nestedstructure/pom.xml
@@ -34,7 +34,6 @@
         <json.path.version>2.9.0</json.path.version>
         <maven.compiler.source>8</maven.compiler.source>
         <maven.compiler.target>8</maven.compiler.target>
-        <nashhorn.core.version>15.4</nashhorn.core.version>
     </properties>
 
     <dependencies>
@@ -79,11 +78,6 @@
                 </exclusion>
             </exclusions>
         </dependency>
-        <dependency>
-            <groupId>org.openjdk.nashorn</groupId>
-            <artifactId>nashorn-core</artifactId>
-            <version>${nashhorn.core.version}</version>
-        </dependency>
         <dependency>
             <groupId>jakarta.ws.rs</groupId>
             <artifactId>jakarta.ws.rs-api</artifactId>
diff --git 
a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/RecordFilterJavaScript.java
 
b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/RecordFilterJavaScript.java
index 3c1139853..aa75fd74d 100644
--- 
a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/RecordFilterJavaScript.java
+++ 
b/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/RecordFilterJavaScript.java
@@ -18,62 +18,28 @@
 
 package org.apache.ranger.authorization.nestedstructure.authorizer;
 
+import org.apache.ranger.plugin.util.GraalScriptEngineCreator;
+import org.apache.ranger.plugin.util.ScriptEngineUtil;
 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.util.Arrays;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
- * Executes an injected javascript command to determine if the user has access 
to the selected record.
+ * Executes a row-filter expression to determine if the user has access to the 
selected record.
  * <p>
- * Prefers the {@code graal.js} engine when present; otherwise falls back to 
Nashorn or other {@code javax.script}
- * engines on the classpath. GraalJS is configured with {@code 
polyglot.js.allowHostAccess} <strong>disabled by
- * default</strong> so record-filter scripts cannot access Java classes or 
host services. To allow Java interop (not
- * recommended), set the system property {@value 
#RANGER_RECORDFILTER_JS_ALLOW_HOST_ACCESS} to {@code true} on the JVM
- * (Graal only).
+ * The engine comes from {@link 
GraalScriptEngineCreator#createNoHostAccessScriptEngine()}, which exposes no 
host
+ * classes and no host methods. Evaluation fails when that engine cannot be 
created.
  * </p>
  */
 public class RecordFilterJavaScript {
     private static final Logger logger = 
LoggerFactory.getLogger(RecordFilterJavaScript.class);
 
-    /**
-     * When set to {@code true}, enables {@code polyglot.js.allowHostAccess} 
for the GraalJS engine used by record
-     * filters. Default is off; this must be an explicit, reviewed decision.
-     */
-    public static final String RANGER_RECORDFILTER_JS_ALLOW_HOST_ACCESS = 
"ranger.nestedstructure.recordfilter.js.allowHostAccess";
-
-    private static final AtomicBoolean LOGGED_HOST_ACCESS_WARNING = new 
AtomicBoolean();
-
-    /**
-     * Supported JavaScript engine names, in order of preference.
-     * Graal.js is preferred for its security model and ECMAScript compliance.
-     * Nashorn is available as a fallback on older JVMs.
-     * js and JavaScript are generic names that may map to available engines.
-     */
-    private static final String ENGINE_GRAAL_JS = "graal.js";
-    private static final String ENGINE_NASHORN = "nashorn";
-    private static final String ENGINE_JS = "js";
-    private static final String ENGINE_JAVASCRIPT = "JavaScript";
-    private static final String ENGINE_JAVASCRIPT_LOWERCASE = "javascript";
-
-    private static final String[] SUPPORTED_ENGINES = {
-            ENGINE_GRAAL_JS,
-            ENGINE_NASHORN,
-            ENGINE_JS,
-            ENGINE_JAVASCRIPT,
-            ENGINE_JAVASCRIPT_LOWERCASE
-    };
-
     private RecordFilterJavaScript() {
     }
 
@@ -107,49 +73,27 @@ public static boolean filterRow(String user, String 
filterExpr, String jsonStrin
             throw new MaskingException("cannot process filter expression: 
blocked by script safety checks: " + filterExpr);
         }
 
-        ClassLoader clsLoader = Thread.currentThread().getContextClassLoader();
-        ScriptEngineManager mgr = new ScriptEngineManager(clsLoader);
-        ScriptEngine        engine = resolveJavaScriptEngine(mgr);
-
-        if (isGraalJsEngine(engine)) {
-            try {
-                Map<String, Boolean> graalVmConfigs = new HashMap<>();
-
-                boolean allowHost = Boolean.parseBoolean(
-                        
System.getProperty(RANGER_RECORDFILTER_JS_ALLOW_HOST_ACCESS, "false"));
-                graalVmConfigs.put("polyglot.js.allowHostAccess", allowHost);
-                if (allowHost && 
LOGGED_HOST_ACCESS_WARNING.compareAndSet(false, true)) {
-                    logger.warn(
-                            "{}=true: GraalJS host/Java interop is enabled for 
nested-structure record filter scripts. "
-                                    + "Use only in a tightly controlled 
environment.",
-                            RANGER_RECORDFILTER_JS_ALLOW_HOST_ACCESS);
-                }
-                graalVmConfigs.put("polyglot.js.nashorn-compat", Boolean.TRUE);
-
-                Bindings engBindings = 
engine.getBindings(ScriptContext.ENGINE_SCOPE);
-                engBindings.putAll(graalVmConfigs);
-                engine.setBindings(engBindings, ScriptContext.ENGINE_SCOPE);
-            } catch (Throwable t) {
-                logger.warn("RecordFilterJavaScript.filterRow(): failed to 
apply GraalJS options for engine graal.js", t);
-            }
-        }
+        ScriptEngine engine = 
GraalScriptEngineCreator.createNoHostAccessScriptEngine();
 
         if (engine == null) {
-            throw new MaskingException("No JavaScript engine (graal.js, 
nashorn, or js) is available on the classpath");
+            throw new MaskingException("unable to evaluate filter expression: 
script engine is not available");
         }
 
         logger.debug("filterExpr: {}", filterExpr);
 
         // convert the given JSON string to JavaScript object, which the 
filterExpr expects, and then exec the filterExpr
-        String script = " var jsonAttr = JSON.parse(jsonString); "
+        String script = ScriptEngineUtil.SCRIPT_SAFE_PREEXEC
+                + " var jsonAttr = JSON.parse(jsonString); "
                 + NASHORN_STYLE_STRING_EQUALS_SHIM
                 + " "
                 + NASHORN_POLYFILL_ARRAY_PROTOTYPE_INCLUDES
                 + " "
                 + filterExpr;
 
+        Bindings bindings = null;
+
         try {
-            Bindings bindings = engine.createBindings();
+            bindings = engine.createBindings();
 
             bindings.put("jsonString", jsonString);
             bindings.put("user", user);
@@ -162,31 +106,20 @@ public static boolean filterRow(String user, String 
filterExpr, String jsonStrin
             return hasAccess;
         } catch (Exception e) {
             throw new MaskingException("unable to properly evaluate filter 
expression: " + filterExpr, e);
+        } finally {
+            closeQuietly(bindings);
+            GraalScriptEngineCreator.closeScriptEngine(engine);
         }
     }
 
-    private static ScriptEngine resolveJavaScriptEngine(ScriptEngineManager 
mgr) {
-        for (String engineName : SUPPORTED_ENGINES) {
-            ScriptEngine engine = mgr.getEngineByName(engineName);
-
-            if (engine != null) {
-                return engine;
+    private static void closeQuietly(Object resource) {
+        if (resource instanceof AutoCloseable) {
+            try {
+                ((AutoCloseable) resource).close();
+            } catch (Exception e) {
+                logger.debug("failed to close script engine resource", e);
             }
         }
-
-        return null;
-    }
-
-    private static boolean isGraalJsEngine(ScriptEngine engine) {
-        if (engine == null) {
-            return false;
-        }
-
-        try {
-            return 
ENGINE_GRAAL_JS.equalsIgnoreCase(engine.getFactory().getEngineName());
-        } catch (Exception e) {
-            return false;
-        }
     }
 
     /**
diff --git 
a/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestRecordFilterJavaScript.java
 
b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestRecordFilterJavaScript.java
index 173c44f30..38f1a5020 100644
--- 
a/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestRecordFilterJavaScript.java
+++ 
b/plugin-nestedstructure/src/test/java/org/apache/ranger/authorization/nestedstructure/authorizer/TestRecordFilterJavaScript.java
@@ -20,11 +20,8 @@
 package org.apache.ranger.authorization.nestedstructure.authorizer;
 
 import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.Test;
 
-import javax.script.ScriptEngineManager;
-
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Paths;
@@ -40,28 +37,22 @@ public void testArbitraryCommand() {
                 RecordFilterJavaScript.filterRow("user", 
"this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec(\"/Applications/Spotify.app/Contents/MacOS/Spotify\")')",
 TestJsonManipulator.bigTester));
     }
 
-    /**
-     * Required security contract: by default, record-filter scripts must not 
be able to use Java/host APIs to create
-     * or modify local files. Do not add a parallel test that asserts a file 
is created when host access is opt-in; that
-     * normalizes filesystem side effects in CI. The opt-in flag exists for 
exceptional deployments only.
-     */
     @Test
-    public void 
testJavaInteropCannotWriteFileWhenHostAccessDisabledByDefault() throws 
Exception {
-        Assumptions.assumeTrue(
-                new ScriptEngineManager().getEngineByName("graal.js") != null,
-                "GraalJS not on this classpath: skip (Nashorn allows Java by 
default, so the secure-default contract is not asserted here).");
-
+    public void testJavaInteropCannotWriteFile() throws Exception {
         Files.deleteIfExists(Paths.get("omg.txt"));
-        
System.clearProperty(RecordFilterJavaScript.RANGER_RECORDFILTER_JS_ALLOW_HOST_ACCESS);
 
         assertThrows(MaskingException.class, () -> 
RecordFilterJavaScript.filterRow("user", "bufferedWriter = new 
java.io.BufferedWriter(new java.io.FileWriter('omg.txt'));\n" +
                 "            bufferedWriter.write(\"Writing line one to 
file\"); bufferedWriter.close;", TestJsonManipulator.bigTester));
         assertFalse(Files.exists(Paths.get("omg.txt")));
     }
 
+    @Test
+    public void testEngineAndContextAreCleared() {
+        assertTrue(RecordFilterJavaScript.filterRow("user", "engine === null 
&& context === null", TestJsonManipulator.bigTester));
+    }
+
     @AfterEach
     public void cleanupTest() throws IOException {
-        
System.clearProperty(RecordFilterJavaScript.RANGER_RECORDFILTER_JS_ALLOW_HOST_ACCESS);
         Files.deleteIfExists(Paths.get("omg.txt"));
     }
 

Reply via email to