
CVE-2025-59059: Fehlzugeschriebene RCE-Korrektur der statischen Analyse in Apache Ranger
CVE: CVE-2025-59059
Betroffene Versionen: Apache Ranger <= 2.7.0
Behoben in: Apache Ranger 2.8.0
CVSS (offiziell): 9,8 Kritisch
Tatsächlicher Schweregrad (argumentiert): ~6,0 Mittel – siehe Analyse unten
Dieser Artikel beschreibt keinen neuen Exploit oder einen funktionierenden PoC. Es geht darum, die Fehler in der öffentlichen Offenlegung von CVE-2025-59059 zu korrigieren – konkret zwei Dinge: Die falsche Klasse wurde als anfällige Komponente genannt, und der CVSS-Score spiegelt nicht die tatsächlichen Einschränkungen der Ausnutzbarkeit wider. Alles basiert auf statischer Codeanalyse des Apache Ranger 2.7.0-Quellcodes und dem Patch-Diff, der in 2.8.0 eingeflossen ist.
Das offizielle Advisory lautet:
„Remote Code Execution Vulnerability in NashornScriptEngineCreator is reported in Apache Ranger versions <= 2.7.0."
Dies ist falsch oder zumindest irreführend. NashornScriptEngineCreator ist nicht die anfällige Klasse. Wenn überhaupt, ist sie die besser gehärtete der beiden Nashorn-bezogenen Komponenten im Codebase – dazu später mehr.
Apache Ranger verwendet JavaScript-Ausdrücke, um zeilenbasierte Filterrichtlinien auszuwerten. Dabei handelt es sich um Regeln, die entscheiden, ob ein bestimmter Benutzer Zugriff auf einen bestimmten Datensatz in einem Dataset erhält. Um diese Ausdrücke auf der JVM auszuführen, bindet Ranger eine JavaScript-Engine ein. In Versionen <= 2.7.0 ist das Nashorn, Oracles integrierte JS-Engine, die mit JDK 8 bis 14 ausgeliefert wurde.
Ein Punkt, den das Advisory völlig auslässt – und der für die Eingrenzung der Auswirkungen sehr wichtig ist – ist, dass jdk.nashorn.api.scripting.NashornScriptEngineFactory überhaupt nicht zum Ranger-Quellcode gehört. Es ist ein JDK-Bestandteil. Es war in JDK 8 enthalten, in JDK 11 als veraltet markiert und in JDK 15 vollständig entfernt. Diese Sicherheitslücke ist daher nur ausnutzbar, wenn Ranger auf JDK 8 bis 14 läuft. Alles auf JDK 15+ ist nicht betroffen, weil Nashorn dort einfach nicht existiert und ScriptEngineUtil stillschweigend auf GraalJS oder JavaScriptEngineCreator zurückfällt.
Zwei Klassen in Ranger verwenden Nashorn. Sie behandeln es sehr unterschiedlich.
NashornScriptEngineCreator – die genannte, aber sicherereLokation:
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;
}
}
}
Diese Klasse unternimmt tatsächlich drei Dinge, um die Engine zu härten:
--no-java – unterbindet direkten Zugriff auf den Namensraum java.* aus Skripten--no-syntax-extensions – deaktiviert nicht standardmäßige Nashorn-SyntaxRangerClassFilter – blockiert allen Zugriff auf Java-Klassen auf ClassFilter-Ebene, gibt für alles false zurückIst es kugelsicher? Nein. Nashorn-ClassFilter-Umgehungen existieren über Reflektionsketten und java.lang.invoke-Tricks. Aber es ist deutlich stärker eingeschränkt als das, was wir in der anderen Klasse finden.
RecordFilterJavaScript – die tatsächlich anfällige KlasseLokation:
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) {
// prüft nur auf diese eine bestimmte Zeichenkette
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...");
}
// instanziiert Nashorn direkt – ohne --no-java, ohne --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);
...
}
}
Vergleicht man dies mit NashornScriptEngineCreator, ist der Unterschied ziemlich offensichtlich:
--no-java-Flag – der Namensraum java.* ist für Skripte vollständig geöffnet--no-syntax-extensionsfilterExpr.contains("this.engine")filterExpr wird direkt in das Skript concateniert, das Nashorn auswertetDa --no-java nicht übergeben wird, ist der Namensraum java.* vollständig zugänglich. Man braucht überhaupt keine Umgehungstechnik:
var runtime = java.lang.Runtime.getRuntime();
runtime.exec("id");
Kein this.engine irgendwo darin. Die Blacklist ist für diesen Angriffspfad völlig irrelevant.
Interessant ist, dass die Entwickler offenbar mindestens ein Umgehungsmuster kannten. Die Testdatei TestRecordFilterJavaScript.java enthält Folgendes:
RecordFilterJavaScript.filterRow("user",
"this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec(\"/Applications/Spotify.app/Contents/MacOS/Spotify\")')",
...);
Dieser Test existiert, um zu bestätigen, dass this.engine blockiert wird. Was nicht blockiert wurde, ist der direkte java.*-Pfad, der überhaupt kein this.engine benötigt.
RecordFilterJavaScript.filterRow() wird nur von einer Stelle aufgerufen:
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 stammt aus result.getFilterExpr(), das ist der JavaScript-Ausdruck, der in einer Ranger-Zeilenfilterrichtlinie gespeichert ist. Ein Angreifer mit Richtlinien-Admin-Rechten kann diesen Ausdruck auf alles setzen, was er möchte. Wenn dann ein Benutzer auf eine Ressource zugreift, die von dieser Richtlinie gesteuert wird, feuert hasAccessToRecord(), holt filterExpr aus dem Richtlinienspeicher, und filterRow() übergibt es an Nashorn – praktisch ohne Sandboxing.
Angriffskette:
Richtlinien-Admin-Zugriff
|
v
Erstellen/Ändern einer Zeilenfilterrichtlinie für eine NestedStructure-Ressource
|
v
filterExpr gesetzt auf: java.lang.Runtime.getRuntime().exec("...")
|
v
Jeder Benutzer greift auf die Ressource zu → hasAccessToRecord() wird ausgelöst
|
v
filterExpr gelangt in filterRow() → Nashorn wertet es aus → RCE
Der Fix wird als RANGER-4076: Remove Nashorn Script Engine verfolgt, committet am 8. Dezember 2025 von Kishor Gollapalliwar. Entnommen aus dem Apache-Mailinglisten-Commit-Archiv. Drei Dateien geändert, 27 Einfügungen, 81 Löschungen.
commit 923a8473de2985cd389d45062cd4717d5ca13235
Author: Kishor Gollapalliwar
AuthorDate: Mon Dec 8 14:55:56 2025 +0530
RANGER-4076: Remove Nashorn Script Engine
.../plugin/util/NashornScriptEngineCreator.java | 67 ----------------------
.../ranger/plugin/util/ScriptEngineUtil.java | 7 +--
.../authorizer/RecordFilterJavaScript.java | 34 ++++++++---
3 files changed, 27 insertions(+), 81 deletions(-)
NashornScriptEngineCreator.java komplett gelöschtdiff --git a/agents-common/src/main/java/org/apache/ranger/plugin/util/NashornScriptEngineCreator.java
deleted file mode 100644
index b890fe85d..000000000
--- a/agents-common/src/main/java/org/apache/ranger/plugin/util/NashornScriptEngineCreator.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package org.apache.ranger.plugin.util;
-
-import jdk.nashorn.api.scripting.ClassFilter;
-import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
-
-public class NashornScriptEngineCreator implements ScriptEngineCreator {
-
- private static final String[] SCRIPT_ENGINE_ARGS = new String[] {
- "--no-java", "--no-syntax-extensions"
- };
- private static final String ENGINE_NAME = "NashornScriptEngine";
-
- @Override
- public ScriptEngine getScriptEngine(ClassLoader clsLoader) {
- ScriptEngine ret = null;
- if (clsLoader == null) {
- clsLoader = getDefaultClassLoader();
- }
- try {
- NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
- ret = factory.getScriptEngine(SCRIPT_ENGINE_ARGS, clsLoader, RangerClassFilter.INSTANCE);
- } catch (Throwable t) {
- LOG.debug("NashornScriptEngineCreator.getScriptEngine(): failed to create engine type {}", ENGINE_NAME, t);
- }
- return ret;
- }
-
- private static class RangerClassFilter implements ClassFilter {
- static final RangerClassFilter INSTANCE = new RangerClassFilter();
-
- @Override
- public boolean exposeToScripts(String className) {
- LOG.warn("script blocked: attempt to use Java class {}", className);
- return false;
- }
- }
-}
ScriptEngineUtil.java – Nashorn aus der Engine-Creator-Kette entferntdiff --git a/agents-common/src/main/java/org/apache/ranger/plugin/util/ScriptEngineUtil.java
--- 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,10 +28,9 @@
public class ScriptEngineUtil {
- private static final String SCRIPT_ENGINE_CREATOR_NASHHORN =
- "org.apache.ranger.plugin.util.NashornScriptEngineCreator";
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_NASHHORN,
- SCRIPT_ENGINE_CREATOR_GRAAL,
- SCRIPT_ENGINE_CREATOR_JS
- };
+ private static final String[] SCRIPT_ENGINE_CREATORS = new String[] {
+ SCRIPT_ENGINE_CREATOR_GRAAL,
+ SCRIPT_ENGINE_CREATOR_JS
+ };
@@ -108,9 +107,7 @@ private static void initScriptEngineCreator(String serviceType) {
} catch (Throwable t) {
boolean logWarn;
- if (creatorClsName.equals(SCRIPT_ENGINE_CREATOR_NASHHORN)) {
- logWarn = JVM_MAJOR_CLASS_VERSION < JVM_MAJOR_CLASS_VERSION_JDK15;
- } else if (creatorClsName.equals(SCRIPT_ENGINE_CREATOR_GRAAL)) {
+ if (creatorClsName.equals(SCRIPT_ENGINE_CREATOR_GRAAL)) {
logWarn = JVM_MAJOR_CLASS_VERSION >= JVM_MAJOR_CLASS_VERSION_JDK15;
} else {
logWarn = true;
RecordFilterJavaScript.java – der eigentliche FixHier findet die eigentliche sicherheitsrelevante Änderung statt. Der direkte NashornScriptEngineFactory-Aufruf wird durch GraalJS ersetzt, und SecurityFilter verliert seine ClassFilter-Rolle vollständig – wird zu einer einfachen Klasse degradiert, die nur noch die Zeichenkettenprüfung behält.
diff --git a/plugin-nestedstructure/src/main/java/org/apache/ranger/authorization/nestedstructure/authorizer/RecordFilterJavaScript.java
--- a/plugin-nestedstructure/.../RecordFilterJavaScript.java
+++ b/plugin-nestedstructure/.../RecordFilterJavaScript.java
@@ -18,13 +18,16 @@
-import jdk.nashorn.api.scripting.ClassFilter;
-import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
+import javax.script.ScriptContext;
+import javax.script.ScriptEngineManager;
+import java.util.HashMap;
+import java.util.Map;
@@ -54,8 +57,25 @@
if (securityFilter.containsMalware(filterExpr)) {
throw new MaskingException("cannot process filter expression...");
}
- NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
- ScriptEngine engine = factory.getScriptEngine(securityFilter);
+ ClassLoader clsLoader = Thread.currentThread().getContextClassLoader();
+ ScriptEngineManager mgr = new ScriptEngineManager(clsLoader);
+ ScriptEngine engine = mgr.getEngineByName("graal.js");
+
+ if (engine != null) {
+ try {
+ Map<String, Boolean> graalVmConfigs = new HashMap<>();
+ graalVmConfigs.put("polyglot.js.allowHostAccess", Boolean.TRUE);
+ graalVmConfigs.put("polyglot.js.nashorn-compat", Boolean.TRUE);
+
+ Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
+ bindings.putAll(graalVmConfigs);
+ engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
+ } catch (Throwable t) {
+ logger.debug("RecordFilterJavaScript.filterRow(): failed to create engine type {}", "graal.js", t);
+ }
+ }
@@ -83,12 +103,8 @@
- static class SecurityFilter implements ClassFilter {
- @Override
- public boolean exposeToScripts(String s) {
- return false;
- }
-
+ static class SecurityFilter {
boolean containsMalware(String filterExpr) {
return filterExpr.contains("this.engine");
}
}
Einige erwähnenswerte Punkte aus diesem Diff:
SecurityFilter ist kein ClassFilter mehr – verliert seinen Engine-Level-Hook und wird zu einer einfachen Klasse, die nur noch die Zeichenkettenprüfung behältScriptEngineManagernashorn-compat-Modus für Abwärtskompatibilität mit bestehenden Richtlinienausdrückenpolyglot.js.allowHostAccess ist auf true gesetzt – dies ist ein Kompromiss für die Abwärtskompatibilität und verlässt sich auf GraalJS eigenes Sandboxing anstelle eines ClassFilter. Es lohnt sich, dies im Auge zu behalten, wenn einem die Qualität des Fixes wichtig ist.Der Patch macht deutlich, dass RecordFilterJavaScript das eigentliche Sicherheitsziel war. Das Löschen von NashornScriptEngineCreator war eine Bereinigung, nicht der Fix.
Ehrlich gesagt ist es nicht schwer zu verstehen, wie das passieren konnte. NashornScriptEngineCreator befindet sich direkt in agents-common, ist die offensichtlichste Nashorn-Klasse im gesamten Codebase, und ein einfaches Grep nach Nashorn würde sie sofort aufspüren. RecordFilterJavaScript hingegen lebt in plugin-nestedstructure, einem separaten Submodul, und der Klassenname enthält keinerlei Hinweis auf Nashorn. Wenn jemand eine schnelle Sichtung durchführt, ohne den Code-Pfad tatsächlich zu verfolgen, würde er wahrscheinlich auf NashornScriptEngineCreator stoßen und dort aufhören.
Das scheint hier passiert zu sein. Die anfällige Klasse und die Klasse, die im Advisory genannt wurde, sind zwei verschiedene Dinge.
Eine 9,8 impliziert netzwerkerreichbar, keine Authentifizierung erforderlich, keine Benutzerinteraktion nötig. Die Realität sieht ganz anders aus:
| Faktor | Realität |
|---|---|
| Authentifizierung | erfordert Richtlinien-Admin-Rechte in Ranger |
| Plugin-Erfordernis | plugin-nestedstructure ist nicht standardmäßig aktiviert |
| JDK-Einschränkung | nur auf JDK 8–14 ausnutzbar, JDK 15+ ist nicht betroffen |
| Netzzugänglichkeit | FOFA-Fingerprinting ergab ~35 öffentlich zugängliche Ranger-Instanzen |
Nichts davon spiegelt sich im Advisory wider. Um dies tatsächlich auszunutzen, benötigt man Richtlinien-Admin-Zugriff, was eine erhebliche Privilegienstufe innerhalb einer Ranger-Bereitstellung darstellt. Dies ist keine nicht authentifizierte RCE. Unter Berücksichtigung der Authentifizierungsanforderung und des nicht standardmäßigen Plugins wäre ein Wert im Bereich 6,0–7,0 ein ehrlicherer Score.
| Behauptung im Advisory | Tatsächlicher Befund | |
|---|---|---|
| Anfällige Klasse | NashornScriptEngineCreator | RecordFilterJavaScript |
| Angriffsart | Nicht authentifizierte RCE | Authentifizierte RCE (Richtlinien-Admin erforderlich) |
| Betroffenes JDK | Nicht spezifiziert | Nur JDK 8–14 |
| Erforderliches Plugin | Nicht spezifiziert | plugin-nestedstructure (nicht standard) |
| CVSS | 9,8 Kritisch | ~6,0 Mittel (argumentiert) |
| Internetzugänglichkeit | Nicht spezifiziert | ~35 Instanzen (FOFA) |
Die Schwachstelle ist real, und ein Upgrade auf 2.8.0 ist der richtige Schritt. Aber das Advisory nennt die falsche Klasse, und der Schweregrad-Score spiegelt nicht die tatsächlich erforderlichen Bedingungen für die Ausnutzung wider.