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"));
}