
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: