
CVE-2025-59059: Corrección de análisis estático de RCE mal atribuido en Apache Ranger
CVE: CVE-2025-59059
Versiones afectadas: Apache Ranger <= 2.7.0
Corregido en: Apache Ranger 2.8.0
CVSS (oficial): 9.8 Crítico
Severidad real (argumentada): ~6.0 Media — ver análisis abajo
Este artículo no trata de un nuevo exploit ni de un PoC funcional. El objetivo es corregir lo que está mal en la divulgación pública de CVE-2025-59059, específicamente dos cosas: la clase incorrecta fue nombrada como el componente vulnerable, y la puntuación CVSS no refleja las limitaciones reales de explotabilidad. Todo lo que aquí se expone se basa en el análisis estático del código fuente de Apache Ranger 2.7.0 y el diff del parche aplicado en 2.8.0.
El aviso oficial dice:
"Se reporta una Vulnerabilidad de Ejecución Remota de Código en NashornScriptEngineCreator en versiones de Apache Ranger <= 2.7.0."
Esto es incorrecto, o al menos engañoso. NashornScriptEngineCreator no es la clase vulnerable. Si acaso, es el componente mejor reforzado de los dos relacionados con Nashorn en el código base — como veremos a continuación.
Apache Ranger usa expresiones JavaScript para evaluar políticas de filtro a nivel de fila. Son reglas que deciden si un usuario determinado tiene acceso a un registro específico en un conjunto de datos. Para ejecutar esas expresiones en la JVM, Ranger incluye un motor JavaScript. En versiones <= 2.7.0 ese motor es Nashorn, el motor JS integrado de Oracle que venía con JDK 8 hasta 14.
Algo que el aviso omite por completo — y que importa mucho para dimensionar el impacto — es que jdk.nashorn.api.scripting.NashornScriptEngineFactory no forma parte del código fuente de Ranger. Es una clase incorporada en el JDK. Estaba incluida en JDK 8, fue deprecada en JDK 11 y eliminada por completo en JDK 15. Por lo tanto, esta vulnerabilidad solo es explotable si se ejecuta Ranger en JDK 8 a 14. Cualquier instalación en JDK 15+ no se ve afectada porque Nashorn simplemente no existe allí, y ScriptEngineUtil recurre silenciosamente a GraalJS o JavaScriptEngineCreator.
Dos clases en Ranger usan Nashorn. Lo tratan de forma muy diferente.
NashornScriptEngineCreator — la nombrada pero más seguraUbicada en:
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;
}
}
}
Esta clase realmente hace tres cosas para reforzar el motor:
--no-java — corta el acceso directo al espacio de nombres java.* desde dentro de los scripts.--no-syntax-extensions — deshabilita las extensiones de sintaxis no estándar de Nashorn.RangerClassFilter — bloquea todo acceso a clases Java a nivel de ClassFilter, devolviendo false para todo.¿Es a prueba de balas? No. Existen evasiones de ClassFilter de Nashorn a través de cadenas de reflexión y trucos con java.lang.invoke. Pero está significativamente más limitado que lo que encontramos en la otra clase.
RecordFilterJavaScript — la clase realmente vulnerableUbicada en:
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) {
// solo verifica esta cadena específica
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...");
}
// instancia Nashorn directamente — sin --no-java, sin --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);
...
}
}
Compárese con NashornScriptEngineCreator y la diferencia es bastante obvia:
--no-java — el espacio de nombres java.* está completamente abierto para los scripts.--no-syntax-extensions.filterExpr.contains("this.engine").filterExpr se concatena directamente en el script que Nashorn evalúa.Debido a que no se pasa --no-java, el espacio de nombres java.* es totalmente accesible. No se necesita ninguna técnica de evasión:
var runtime = java.lang.Runtime.getRuntime();
runtime.exec("id");
En ninguna parte aparece this.engine. La lista negra es completamente irrelevante para esta vía de ataque.
Lo interesante es que los desarrolladores claramente conocían al menos un patrón de evasión. El archivo de prueba TestRecordFilterJavaScript.java contiene esto:
RecordFilterJavaScript.filterRow("user",
"this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec(\"/Applications/Spotify.app/Contents/MacOS/Spotify\")')",
...);
Esa prueba existe para confirmar que this.engine es bloqueado. Lo que no se bloqueó es la ruta directa a java.*, que no necesita this.engine en absoluto.
RecordFilterJavaScript.filterRow() solo se llama desde un lugar:
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 proviene de result.getFilterExpr() — esa es la expresión JavaScript almacenada en una política de filtro de fila de Ranger. Un atacante con privilegios de administrador de políticas puede establecer esa expresión como desee. Cuando cualquier usuario accede a un recurso gobernado por esa política, se dispara hasAccessToRecord(), obtiene filterExpr del almacén de políticas y filterRow() se lo pasa a Nashorn prácticamente sin sandbox.
Cadena de ataque:
Acceso administrador de políticas
|
v
Crear/modificar una política de filtro de fila en un recurso nestedstructure
|
v
filterExpr establecido como: java.lang.Runtime.getRuntime().exec("...")
|
v
Cualquier usuario accede al recurso → se dispara hasAccessToRecord()
|
v
filterExpr fluye hacia filterRow() → Nashorn lo evalúa → RCE
La corrección se registra como RANGER-4076: Remove Nashorn Script Engine, commiteada el 8 de diciembre de 2025 por Kishor Gollapalliwar. Extraída del archivo de commits de la lista de correo de Apache. Tres archivos modificados, 27 inserciones, 81 eliminaciones.
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 eliminado por completodiff --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 eliminado de la cadena de creadores de motoresdiff --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 — la corrección realAquí es donde está el cambio de seguridad real. La llamada directa a NashornScriptEngineFactory se reemplaza por GraalJS, y SecurityFilter pierde su rol de ClassFilter por completo — pasa a ser una clase simple con solo la comprobación de cadena.
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");
}
}
Algunas cosas que vale la pena destacar de este diff:
SecurityFilter ya no es un ClassFilter — pierde su enlace a nivel de motor y se convierte en una clase normal con solo la comprobación de cadena.ScriptEngineManager.nashorn-compat para mantener la retrocompatibilidad con las expresiones de políticas existentes.polyglot.js.allowHostAccess está configurado como true — esto es una concesión por retrocompatibilidad, y depende del sandbox propio de GraalJS en lugar de un ClassFilter. Vale la pena tenerlo en cuenta si te importa la calidad de la corrección.El parche deja claro que RecordFilterJavaScript era el objetivo de seguridad real. Eliminar NashornScriptEngineCreator fue una limpieza, no la corrección.
Honestamente, no es difícil ver cómo sucedió. NashornScriptEngineCreator está justo en agents-common, es la clase con nombre Nashorn más obvia en todo el código base, y un simple grep por Nashorn la encontraría de inmediato. RecordFilterJavaScript, en cambio, vive en plugin-nestedstructure, que es un submódulo separado, y no hay nada en el nombre de la clase que indique Nashorn. Si alguien hiciera un triaje rápido sin seguir realmente la ruta del código, probablemente aterrizaría en NashornScriptEngineCreator y se detendría allí.
Parece que eso es lo que ocurrió aquí. La clase vulnerable y la clase que se nombró en el aviso son dos cosas diferentes.
Un 9.8 implica accesible por red, sin autenticación, sin interacción del usuario. La realidad es bastante diferente:
| Factor | Realidad |
|---|---|
| Autenticación | Requiere privilegios de administrador de políticas en Ranger |
| Requisito de plugin | plugin-nestedstructure no está habilitado por defecto |
| Restricción de JDK | Solo explotable en JDK 8–14; JDK 15+ no se ve afectado |
| Exposición en red | La huella de FOFA devolvió ~35 instancias de Ranger expuestas públicamente |
Nada de esto se refleja en el aviso. Para explotar esto realmente necesitas acceso de administrador de políticas, que es un nivel de privilegio significativo dentro de un despliegue de Ranger. Esto no es RCE sin autenticación. Teniendo en cuenta el requisito de autenticación y el plugin no predeterminado, una puntuación en el rango 6.0–7.0 sería más honesta.
| Afirmación del aviso | Hallazgo real | |
|---|---|---|
| Clase vulnerable | NashornScriptEngineCreator | RecordFilterJavaScript |
| Tipo de ataque | RCE sin autenticación | RCE autenticado (requiere admin de políticas) |
| JDK afectado | No especificado | Solo JDK 8–14 |
| Plugin requerido | No especificado | plugin-nestedstructure (no predeterminado) |
| CVSS | 9.8 Crítico | ~6.0 Medio (argumentado) |
| Exposición en Internet | No especificada | ~35 instancias (FOFA) |
La vulnerabilidad es real y actualizar a 2.8.0 es la decisión correcta. Pero el aviso se equivoca en la clase y la puntuación de gravedad no refleja lo que realmente se necesita para explotarla.