
CVE-2025-59059: Correção de Análise Estática do RCE Atribuído Incorretamente no Apache Ranger
CVE: CVE-2025-59059
Versões afetadas: Apache Ranger <= 2.7.0
Corrigido em: Apache Ranger 2.8.0
CVSS (oficial): 9.8 Crítica
Severidade real (argumentada): ~6.0 Média — veja análise abaixo
Este artigo não é sobre um novo exploit ou um PoC funcional. O objetivo aqui é corrigir o que está errado na divulgação pública da CVE-2025-59059, especificamente duas coisas: a classe errada foi nomeada como componente vulnerável, e a pontuação CVSS não reflete as restrições reais de explorabilidade. Tudo aqui é baseado na análise estática do código-fonte do Apache Ranger 2.7.0 e no diff do patch que entrou na versão 2.8.0.
O aviso oficial diz:
"Vulnerabilidade de Execução Remota de Código em NashornScriptEngineCreator é relatada no Apache Ranger versões <= 2.7.0."
Isso está errado, ou pelo menos é enganoso. NashornScriptEngineCreator não é a classe vulnerável. Na verdade, é a mais endurecida dos dois componentes relacionados a Nashorn no código — o que veremos a seguir.
O Apache Ranger usa expressões JavaScript para avaliar políticas de filtro no nível de linha. Essas são regras que decidem se um determinado usuário tem acesso a um registro específico em um conjunto de dados. Para executar essas expressões na JVM, o Ranger incorpora um mecanismo JavaScript. Nas versões <= 2.7.0, esse mecanismo é o Nashorn, o mecanismo JS embutido da Oracle que acompanhava o JDK 8 até o 14.
Uma coisa que o aviso omite completamente — e isso importa muito para dimensionar o impacto — é que jdk.nashorn.api.scripting.NashornScriptEngineFactory não faz parte do código-fonte do Ranger. É um componente embutido do JDK. Foi incluído no JDK 8, descontinuado no JDK 11 e totalmente removido no JDK 15. Portanto, essa vulnerabilidade só é alcançável se você estiver executando o Ranger em JDK 8 a 14. Qualquer ambiente no JDK 15+ não é afetado, pois o Nashorn simplesmente não existe nele, e o ScriptEngineUtil apenas cai silenciosamente para o GraalJS ou JavaScriptEngineCreator.
Duas classes no Ranger usam Nashorn. Elas o tratam de forma muito diferente.
NashornScriptEngineCreator — a nomeada, porém mais seguraLocalizada em:
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;
}
}
}
Essa classe na verdade faz três coisas para endurecer o mecanismo:
--no-java — corta o acesso direto ao namespace java.* de dentro dos scripts--no-syntax-extensions — desabilita a sintaxe não padrão do NashornRangerClassFilter — bloqueia todo acesso a classes Java no nível do ClassFilter, retorna false para tudoÉ à prova de balas? Não. Existem bypasses do ClassFilter do Nashorn por meio de cadeias de reflexão e truques com java.lang.invoke. Mas é significativamente mais restrito do que o que encontramos na outra classe.
RecordFilterJavaScript — a classe realmente vulnerávelLocalizada em:
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);
...
}
}
Compare isso com NashornScriptEngineCreator e a diferença é bem óbvia:
--no-java — o namespace java.* está totalmente aberto para scripts--no-syntax-extensionsfilterExpr.contains("this.engine")filterExpr é concatenado diretamente no script que o Nashorn avaliaComo --no-java não é passado, o namespace java.* fica totalmente acessível. Você não precisa de nenhuma técnica de bypass:
var runtime = java.lang.Runtime.getRuntime();
runtime.exec("id");
Não há this.engine em lugar nenhum. A blacklist é totalmente irrelevante para esse caminho de ataque.
O interessante é que os desenvolvedores claramente conheciam pelo menos um padrão de bypass. O arquivo de teste TestRecordFilterJavaScript.java contém isto:
RecordFilterJavaScript.filterRow("user",
"this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec(\"/Applications/Spotify.app/Contents/MacOS/Spotify\")')",
...);
Esse teste existe para confirmar que this.engine é bloqueado. O que não foi bloqueado é o caminho direto de java.*, que não precisa de this.engine de forma alguma.
RecordFilterJavaScript.filterRow() só é chamado de um 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 vem de result.getFilterExpr() — essa é a expressão JavaScript armazenada em uma política de filtro de linha do Ranger. Um atacante com privilégios de administrador de políticas pode definir essa expressão como quiser. Quando qualquer usuário acessa um recurso regido por essa política, hasAccessToRecord() é acionado, busca filterExpr do armazenamento de políticas e filterRow() a entrega ao Nashorn praticamente sem sandbox.
Cadeia de ataque:
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