
CVE-2025-59059: Misattributed RCE in Apache Ranger Static Analysis Correction
CVE: CVE-2025-59059
Affected versions: Apache Ranger <= 2.7.0
Fixed in: Apache Ranger 2.8.0
CVSS (official): 9.8 Critical
Actual severity (argued): ~6.0 Medium — see analysis below
this write-up isnt about a new exploit or a working PoC. the point here is to correct whats wrong with the public disclosure on CVE-2025-59059 specifically two things: the wrong class got named as the vulnerable component, and the CVSS score doesnt reflect the actual exploitability constraints. everything here is based on static code analysis of the Apache Ranger 2.7.0 source and the patch diff that went into 2.8.0.
the official advisory reads:
"Remote Code Execution Vulnerability in NashornScriptEngineCreator is reported in Apache Ranger versions <= 2.7.0."
this is wrong, or at least misleading. NashornScriptEngineCreator is not the vulnerable class. if anything its the better hardened of the two Nashorn-related components in the codebase — which we'll get into.
Apache Ranger uses JavaScript expressions to evaluate row-level filter policies. these are rules that decide whether a given user gets access to a specific record in a dataset. to actually run those expressions on the JVM, Ranger embeds a JavaScript engine. in versions <= 2.7.0 that engine is Nashorn, Oracles built-in JS engine that shipped with JDK 8 through 14.
one thing the advisory completly omits — and this matters a lot for scoping impact — is that jdk.nashorn.api.scripting.NashornScriptEngineFactory is not part of Rangers source code at all. its a JDK built-in. it was included in JDK 8, deprecated in JDK 11, and fully removed in JDK 15. so this vuln is only reachable if youre running Ranger on JDK 8 through 14. anything on JDK 15+ isnt affected because Nashorn simply doesnt exist there, and ScriptEngineUtil just falls through to GraalJS or JavaScriptEngineCreator silently.
two classes in Ranger use Nashorn. they treat it very differently.
NashornScriptEngineCreator — the named but safer onelocated at:
agents-common/src/main/java/org/apache/ranger/plugin/util/NashornScriptEngineCreator.java
public class NashornScriptEngineCreator implements ScriptEngineCreator {
private static final String[] SCRIPT_ENGINE_ARGS = new String[] {
"--no-java", "--no-syntax-extensions"
};
@Override
public ScriptEngine getScriptEngine(ClassLoader clsLoader) {
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ret = factory.getScriptEngine(SCRIPT_ENGINE_ARGS, clsLoader, RangerClassFilter.INSTANCE);
...
}
private static class RangerClassFilter implements ClassFilter {
@Override
public boolean exposeToScripts(String className) {
LOG.warn("script blocked: attempt to use Java class {}", className);
return false;
}
}
}
this class actually does three things to harden the engine:
--no-java — cuts off direct access to the java.* namespace from inside scripts--no-syntax-extensions — disables non-standard Nashorn syntaxRangerClassFilter — blocks all Java class access at the ClassFilter level, returns false for everythingis it bulletproof? no. Nashorn ClassFilter bypasses exist through reflection chains and java.lang.invoke tricks. but its meaningfully more locked down than what we find in the other class.
RecordFilterJavaScript the actual vulnerable classlocated at:
plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/RecordFilterJavaScript.java
public class RecordFilterJavaScript {
static class SecurityFilter implements ClassFilter {
@Override
public boolean exposeToScripts(String s) {
return false;
}
boolean containsMalware(String filterExpr) {
// only checks for this one specific string
return filterExpr.contains("this.engine");
}
}
public static boolean filterRow(String user, String filterExpr, String jsonString) {
SecurityFilter securityFilter = new SecurityFilter();
if (securityFilter.containsMalware(filterExpr)) {
throw new MaskingException("cannot process filter expression due to security concern...");
}
// instantiates Nashorn directly — no --no-java, no --no-syntax-extensions
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ScriptEngine engine = factory.getScriptEngine(securityFilter);
String script = " jsonAttr = JSON.parse(jsonString); " + filterExpr;
Bindings bindings = engine.createBindings();
bindings.put("jsonString", jsonString);
bindings.put("user", user);
boolean hasAccess = (boolean) engine.eval(script, bindings);
...
}
}
compare this to NashornScriptEngineCreator and the difference is pretty obvious:
--no-java flag java.* namespace is fully open to scripts--no-syntax-extensionsfilterExpr.contains("this.engine")filterExpr gets concatenated directly into the script that Nashorn evaluatesbecause --no-java isnt passed, the java.* namespace is fully accessible. you dont need any bypass technique at all:
var runtime = java.lang.Runtime.getRuntime();
runtime.exec("id");
no this.engine anywhere in there. the blacklist is completely irrelevent to this attack path.
whats interesting is that the developers clearly knew about at least one bypass pattern. the test file TestRecordFilterJavaScript.java has this:
RecordFilterJavaScript.filterRow("user",
"this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec(\"/Applications/Spotify.app/Contents/MacOS/Spotify\")')",
...);
that test exists to confirm this.engine gets blocked. what wasnt blocked is the direct java.* path, which doesnt need this.engine at all.
RecordFilterJavaScript.filterRow() is only called from one place:
plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/NestedStructureAuthorizer.java
private boolean hasAccessToRecord(String schema, String user, ..., String jsonString, ...) {
RangerAccessResult result = plugin.evalRowFilterPolicies(request, null);
if (result.isRowFilterEnabled()) {
String filterExpr = result.getFilterExpr();
ret = RecordFilterJavaScript.filterRow(user, filterExpr, jsonString);
}
return ret;
}
filterExpr comes from result.getFilterExpr() thats the JavaScript expression stored in a Ranger row filter policy. an attacker with policy admin privileges can set that expression to whatever they want. when any user then hits a resource governed by that policy, hasAccessToRecord() fires, pulls filterExpr from the policy store, and filterRow() hands it to Nashorn with basically no sandboxing.
attack chain:
Policy admin access
|
v
Create/modify a row filter policy on a nestedstructure resource
|
v
filterExpr set to: java.lang.Runtime.getRuntime().exec("...")
|
v
Any user accesses the resource → hasAccessToRecord() triggered
|
v
filterExpr flows into filterRow() → Nashorn evaluates it → RCE
the fix is tracked as RANGER-4076: Remove Nashorn Script Engine, commited December 8, 2025 by Kishor Gollapalliwar. pulled from the Apache mailing list commit archive. three files changed, 27 insertions, 81 deletions.
commit 923a8473de2985cd389d45062cd4717d5ca13235
Author: Kishor Gollapalliwar
AuthorDate: Mon Dec 8 14:55:56 2025 +0530