
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: