
CVE-2025-59059 : Correction d'analyse statique pour une RCE mal attribuée dans Apache Ranger
CVE : CVE-2025-59059
Versions affectées : Apache Ranger <= 2.7.0
Corrigé dans : Apache Ranger 2.8.0
CVSS (officiel) : 9.8 Critique
Sévérité réelle (argumentée) : ~6.0 Moyen — voir analyse ci-dessous
Ce billet ne parle pas d'un nouvel exploit ni d'un PoC fonctionnel. Le but ici est de corriger ce qui cloche dans la divulgation publique de CVE-2025-59059, plus précisément deux choses : la mauvaise classe a été désignée comme composant vulnérable, et le score CVSS ne reflète pas les contraintes réelles d'exploitabilité. Tout ceci repose sur une analyse statique du code source d'Apache Ranger 2.7.0 et sur le diff du correctif intégré dans 2.8.0.
L'avis officiel indique :
« Remote Code Execution Vulnerability in NashornScriptEngineCreator is reported in Apache Ranger versions <= 2.7.0. »
C'est faux, ou du moins trompeur. NashornScriptEngineCreator n'est pas la classe vulnérable. Si quoi que ce soit, c'est la mieux durcie des deux composants liés à Nashorn dans le codebase — comme nous allons le voir.
Apache Ranger utilise des expressions JavaScript pour évaluer les politiques de filtrage au niveau des lignes. Ce sont des règles qui déterminent si un utilisateur donné peut accéder à un enregistrement spécifique dans un jeu de données. Pour exécuter ces expressions sur la JVM, Ranger embarque un moteur JavaScript. Dans les versions <= 2.7.0, ce moteur est Nashorn, le moteur JS intégré d'Oracle livré avec les JDK 8 à 14.
Une chose que l'avis omet complètement — et qui compte beaucoup pour évaluer le périmètre d'impact — est que jdk.nashorn.api.scripting.NashornScriptEngineFactory ne fait pas du tout partie du code source de Ranger. C'est un composant intégré au JDK. Il a été inclus dans JDK 8, déprécié dans JDK 11 et totalement supprimé dans JDK 15. Cette vulnérabilité n'est donc exploitable que si vous exécutez Ranger sur un JDK 8 à 14. Tout ce qui tourne sur JDK 15+ n'est pas concerné, car Nashorn n'y existe tout simplement pas, et ScriptEngineUtil retombe silencieusement sur GraalJS ou JavaScriptEngineCreator.
Deux classes de Ranger utilisent Nashorn. Elles le traitent de manière très différente.
NashornScriptEngineCreator — la classe nommée mais la plus sûresituée à :
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;
}
}
}
Cette classe fait en réalité trois choses pour durcir le moteur :
--no-java — coupe l'accès direct à l'espace de noms java.* depuis les scripts--no-syntax-extensions — désactive la syntaxe Nashorn non standardRangerClassFilter — bloque tout accès aux classes Java au niveau du ClassFilter, renvoyant false pour toutEst-ce infaillible ? Non. Des contournements du ClassFilter de Nashorn existent via des chaînes de réflexion et des astuces java.lang.invoke. Mais c'est nettement plus verrouillé que ce que l'on trouve dans l'autre classe.
RecordFilterJavaScript — la classe réellement vulnérablesituée à :
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);
...
}
}
Comparez cela avec NashornScriptEngineCreator et la différence est assez évidente :
--no-java : l'espace de noms java.* est entièrement ouvert aux scripts--no-syntax-extensionsfilterExpr.contains("this.engine")filterExpr est concaténée directement dans le script que Nashorn évalueÉtant donné que --no-java n'est pas passé, l'espace de noms java.* est entièrement accessible. Aucune technique de contournement n'est nécessaire :
var runtime = java.lang.Runtime.getRuntime();
runtime.exec("id");
Aucun this.engine là-dedans. La liste noire est totalement hors de propos pour ce vecteur d'attaque.
Ce qui est intéressant, c'est que les développeurs connaissaient visiblement au moins un schéma de contournement. Le fichier de test TestRecordFilterJavaScript.java contient ceci :
RecordFilterJavaScript.filterRow("user",
"this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec(\"/Applications/Spotify.app/Contents/MacOS/Spotify\")')",
...);
Ce test existe pour confirmer que this.engine est bloqué. Ce qui n'a pas été bloqué, c'est le chemin direct java.*, qui n'a pas du tout besoin de this.engine.
RecordFilterJavaScript.filterRow() n'est appelé qu'à un seul endroit :
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 provient de result.getFilterExpr(). C'est l'expression JavaScript stockée dans une politique de filtre de lignes Ranger. Un attaquant disposant des privilèges d'administrateur de politiques peut définir cette expression comme il le souhaite. Lorsqu'un utilisateur accède ensuite à une ressource régie par cette politique, hasAccessToRecord() se déclenche, récupère filterExpr depuis le stockage de politiques et filterRow() le transmet à Nashorn sans véritable sandbox.
chaîne d'attaque :