
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 :
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
Le correctif est référencé sous RANGER-4076: Remove Nashorn Script Engine, commité le 8 décembre 2025 par Kishor Gollapalliwar. Il provient des archives de commits de la liste de diffusion Apache. Trois fichiers modifiés, 27 insertions, 81 suppressions.
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 entièrement supprimédiff --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 retiré de la chaîne de créateurs de moteursdiff --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 — le vrai correctifC'est là qu'intervient le véritable changement de sécurité. L'appel direct à NashornScriptEngineFactory est remplacé par GraalJS, et SecurityFilter perd entièrement son rôle de ClassFilter, étant relégué à une simple classe avec seulement la vérification de chaîne.
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");
}
}
Quelques points à signaler dans ce diff :
SecurityFilter n'est plus un ClassFilter : il perd son accroche au niveau du moteur et devient une simple classe à laquelle il ne reste que la vérification de chaîne.ScriptEngineManager.nashorn-compat pour la compatibilité ascendante avec les expressions de politiques existantes.polyglot.js.allowHostAccess est défini sur true. C'est un compromis pour la compatibilité ascendante, qui s'appuie sur le sandbox propre à GraalJS plutôt que sur un ClassFilter. À surveiller si la qualité du correctif vous importe.Le patch montre clairement que RecordFilterJavaScript était la véritable cible de sécurité. La suppression de NashornScriptEngineCreator était du nettoyage, pas le correctif.
Honnêtement, il n'est pas très difficile de voir comment c'est arrivé. NashornScriptEngineCreator se trouve directement dans agents-common, c'est la classe de tout le codebase dont le nom évoque le plus Nashorn, et un simple grep sur Nashorn la ferait remonter immédiatement. RecordFilterJavaScript, en revanche, vit dans plugin-nestedstructure, un sous-module séparé, et rien dans le nom de la classe n'évoque Nashorn. Si quelqu'un faisait un triage rapide sans suivre réellement le chemin d'exécution, il tomberait probablement sur NashornScriptEngineCreator et s'arrêterait là.
C'est apparemment ce qui s'est passé ici. La classe vulnérable et la classe nommée dans l'avis sont deux choses différentes.
Un 9.8 implique une accessibilité réseau, aucune authentification requise, aucune interaction utilisateur nécessaire. La réalité est bien différente :
| Facteur | Réalité |
|---|---|
| Authentification | nécessite des privilèges d'administrateur de politiques dans Ranger |
| Plugin requis | plugin-nestedstructure n'est pas activé par défaut |
Rien de tout cela n'est reflété dans l'avis. Pour exploiter réellement cette vulnérabilité, vous avez besoin d'un accès administrateur de politiques, ce qui constitue un niveau de privilège significatif dans un déploiement Ranger. Ce n'est pas une RCE sans authentification. En tenant compte de l'exigence d'authentification et du plugin non activé par défaut, un score dans la plage 6.0–7.0 serait plus honnête.
La vulnérabilité est réelle et la mise à niveau vers 2.8.0 est la bonne décision. Mais l'avis se trompe de classe et le score de sévérité ne reflète pas ce qui est réellement nécessaire pour exploiter cette vulnérabilité.
| Contrainte JDK |
| exploitable uniquement sur JDK 8–14, JDK 15+ n'est pas concerné |
| Exposition réseau | L'empreinte FOFA a renvoyé environ 35 instances Ranger exposées publiquement |
| Allégation de l'avis | Constat réel |
|---|
| Classe vulnérable | NashornScriptEngineCreator | RecordFilterJavaScript |
| Type d'attaque | RCE sans authentification | RCE authentifiée (admin de politiques requis) |
| JDK concerné | Non précisé | JDK 8–14 uniquement |
| Plugin requis | Non précisé | plugin-nestedstructure (non par défaut) |
| CVSS | 9.8 Critique | ~6.0 Moyen (argumenté) |
| Exposition Internet | Non précisée | ~35 instances (FOFA) |