
CVE-2025-59059: تصحيح التحليل الساكن لثغرة RCE المنسوبة بشكل خاطئ في Apache Ranger
CVE: CVE-2025-59059
الإصدارات المتأثرة: Apache Ranger <= 2.7.0
تم الإصلاح في: Apache Ranger 2.8.0
CVSS (رسمي): 9.8 حرجة
الخطورة الفعلية (وفق الحجة): ~6.0 متوسطة — انظر التحليل أدناه
هذه المقالة ليست عن استغلال جديد أو PoC عملي. الهدف هنا هو تصحيح ما هو خاطئ في الإفصاح العام عن CVE-2025-59059، وتحديدًا أمران: تمت تسمية الفئة الخاطئة كمكوّن معرّض للثغرة، ودرجة CVSS لا تعكس قيود قابلية الاستغلال الفعلية. كل ما هو مذكور هنا مبني على تحليل ثابت لشفرة Apache Ranger 2.7.0 المصدرية وفرق التصحيح (patch diff) الذي دخل في الإصدار 2.8.0.
النشرة الأمنية الرسمية تقول:
"تم الإبلاغ عن ثغرة تنفيذ الأكواد عن بُعد في NashornScriptEngineCreator في Apache Ranger الإصدارات <= 2.7.0."
هذا خطأ، أو على الأقل مضلل. NashornScriptEngineCreator ليست الفئة المعرّضة للثغرة. إن وُجد شيء، فهي الأكثر تحصينًا بين المكوّنين المرتبطين بـ Nashorn في قاعدة الشيفرة — وسنصل إلى ذلك.
يستخدم Apache Ranger تعبيرات JavaScript لتقييم سياسات تصفية الصفوف (row-level filter policies). وهي قواعد تحدّد ما إذا كان مستخدم معيّن سيحصل على حق الوصول إلى سجلّ محدّد داخل مجموعة بيانات. لتشغيل تلك التعبيرات فعليًا على JVM، يضمّن Ranger محرّك JavaScript. في الإصدارات <= 2.7.0 يكون هذا المحرك هو Nashorn، محرّك JS المدمج من Oracle والذي جاء مع JDK 8 حتى 14.
أمر واحد تحذفه النشرة تمامًا — وهو أمر مهم جدًا لتحديد نطاق الأثر — هو أن jdk.nashorn.api.scripting.NashornScriptEngineFactory ليس جزءًا من شيفرة Ranger المصدرية إطلاقًا. إنه مكوّن مدمج في JDK. فقد كان مشمولًا في JDK 8، وأصبح مهملًا (deprecated) في JDK 11، وأُزيل كليًا في JDK 15. لذلك لا يمكن الوصول إلى هذه الثغرة إلا إذا كنت تشغّل Ranger على JDK 8 حتى 14. أي شيء يعمل على JDK 15+ غير متأثر لأن Nashorn ببساطة غير موجود هناك، وScriptEngineUtil يمرّر بصمت إلى GraalJS أو JavaScriptEngineCreator.
هناك فئتان في Ranger تستخدمان Nashorn، وتتعاملان معه بطريقة مختلفة جدًا.
NashornScriptEngineCreator — الفئة المذكورة لكنها الأكثر أمانًاتقع في:
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;
}
}
}
تقوم هذه الفئة فعليًا بثلاثة أشياء لتقوية المحرك:
--no-java — يقطع الوصول المباشر إلى فضاء الأسماء java.* من داخل السكربتات--no-syntax-extensions — يعطّل صيغة Nashorn غير القياسيةRangerClassFilter — يمنع كل الوصول إلى فئات Java على مستوى ClassFilter، ويعيد false لكل شيءهل هي منيعة تمامًا؟ لا. توجد طرق لتجاوز ClassFilter في Nashorn عبر سلاسل reflection وحيل java.lang.invoke. لكنها أكثر تقييدًا بشكل ملموس مما نجده في الفئة الأخرى.
RecordFilterJavaScript — الفئة المعرّضة للثغرة فعليًاتقع في:
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);
...
}
}
قارن هذا بـ NashornScriptEngineCreator وسيصبح الفرق واضحًا تمامًا:
--no-java، فضاء الأسماء java.* مفتوح بالكامل للسكربتات--no-syntax-extensionsfilterExpr.contains("this.engine")filterExpr يُدمج مباشرة في السكربت الذي يقيّمه Nashornنظرًا لأن --no-java لا يُمرَّر، فإن فضاء الأسماء java.* يمكن الوصول إليه بالكامل. لا تحتاج إلى أي تقنية تجاوز إطلاقًا:
var runtime = java.lang.Runtime.getRuntime();
runtime.exec("id");
لا يوجد this.engine في أي مكان هنا. والقائمة السوداء غير ذات صلة إطلاقًا بمسار الهجوم هذا.
المثير للاهتمام أن المطوّرين كانوا يعرفون بوضوح نمط تجاوز واحدًا على الأقل. ملف الاختبار TestRecordFilterJavaScript.java يحتوي على هذا:
RecordFilterJavaScript.filterRow("user",
"this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec(\"/Applications/Spotify.app/Contents/MacOS/Spotify\")')",
...);
هذا الاختبار موجود للتأكيد على أن this.engine يتم حظره. أما ما لم يتم حظره فهو المسار المباشر java.*، الذي لا يحتاج إلى this.engine إطلاقًا.
RecordFilterJavaScript.filterRow() تُستدعى من مكان واحد فقط:
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 تأتي من result.getFilterExpr()، أي التعبير البرمجي JavaScript المخزّن في سياسة تصفية صفوف في Ranger. يمكن للمهاجم الذي يملك صلاحيات إدارة السياسات (policy admin) ضبط هذا التعبير على ما يشاء. عندما يصل أي مستخدم إلى مورد محكوم بتلك السياسة، يتم تشغيل hasAccessToRecord()، وتسحب filterExpr من مخزن السياسات، وتسلمها filterRow() إلى Nashorn دون أي عزل فعلي تقريبًا.
سلسلة الهجوم:
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
يُتتبَّع الإصلاح باسم RANGER-4076: Remove Nashorn Script Engine، وتم تنفيذه في 8 ديسمبر 2025 بواسطة Kishor Gollapalliwar. سُحب من أرشيف رسائل الالتزامات في القائمة البريدية لـ Apache. ثلاثة ملفات تغيّرت، 27 إضافة، 81 حذفًا.
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 — حُذف بالكامل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 من سلسلة منشئي المحركاتdiff --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 — الإصلاح الفعليهنا يكمن التغيير الأمني الفعلي. استدعاء NashornScriptEngineFactory المباشر يُستبدل بـ GraalJS، وSecurityFilter تفقد دورها كـ ClassFilter تمامًا وتُخفَّض إلى فئة عادية لا تملك سوى فحص النص.
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");
}
}
بعض الأمور الجديرة بالذكر من هذا الفرق:
SecurityFilter لم تعد ClassFilter، فقدت خطافها على مستوى المحرك وأصبحت مجرد فئة عادية بقيت فيها فقط فحص النصScriptEngineManagernashorn-compat من أجل التوافق العكسي مع تعبيرات السياسات الحاليةpolyglot.js.allowHostAccess مضبوط على true — هذه مقايضة من أجل التوافق العكسي، وتعتمد على صندوق الرمل الخاص بـ GraalJS بدلاً من ClassFilter. يجدر مراقبتها إذا كنت تهتم بجودة الإصلاحيجعل التصحيح من الواضح أن RecordFilterJavaScript هي الهدف الأمني الفعلي. حذف NashornScriptEngineCreator كان تنظيفًا، وليس الإصلاح.
بصراحة، ليس من الصعب فهم كيف حدث ذلك. NashornScriptEngineCreator موجودة مباشرة في agents-common، وهي الفئة الأكثر وضوحًا من حيث الاسم المرتبط بـ Nashorn في قاعدة الشيفرة بأكملها، وأي بحث grep بسيط عن Nashorn سيُظهرها فورًا. أما RecordFilterJavaScript فهي تعيش في plugin-nestedstructure، وهو وحدة فرعية منفصلة، ولا يوجد في اسمها أي ما يلمّح إلى Nashorn. أي شخص يقوم بفرز سريع دون تتبّع مسار الشيفرة فعليًا سيستقر غالبًا على NashornScriptEngineCreator ويتوقف عندها.
يبدو أن هذا هو ما حدث هنا. الفئة المعرّضة للثغرة والفئة التي ذُكرت في النشرة شيئان مختلفان.
درجة 9.8 تعني قابلية الوصول عبر الشبكة، دون الحاجة إلى مصادقة، ودون تفاعل من المستخدم. الواقع مختلف تمامًا:
| العامل | الواقع |
|---|---|
| المصادقة | تتطلب صلاحيات إدارة السياسات في Ranger |
| اشتراط البرنامج المساعد | plugin-nestedstructure ليس مفعّلًا افتراضيًا |
| قيد JDK | قابل للاستغلال فقط على JDK 8–14، وJDK 15+ غير متأثر |
| التعرض عبر الشبكة | بصمة FOFA أرجعَت حوالي 35 مثيل Ranger مكشوفًا علنًا |
لا ينعكس أي من ذلك في النشرة. لاستغلال هذا فعليًا تحتاج إلى صلاحيات إدارة السياسات، وهو مستوى امتياز كبير داخل نشر Ranger. هذه ليست RCE بدون مصادقة. عند احتساب شرط المصادقة والبرنامج المساعد غير الافتراضي، فإن درجة في نطاق 6.0–7.0 ستكون أكثر صدقًا.
| ادعاء النشرة | النتيجة الفعلية | |
|---|---|---|
| الفئة المعرّضة | NashornScriptEngineCreator | RecordFilterJavaScript |
| نوع الهجوم | RCE بدون مصادقة | RCE بمصادقة (تتطلب صلاحيات إدارة السياسات) |
| JDK المتأثر | غير محدد | JDK 8–14 فقط |
| البرنامج المساعد المطلوب | غير محدد | plugin-nestedstructure (غير افتراضي) |
| CVSS | 9.8 حرجة | ~6.0 متوسطة (وفق الحجة) |
| التعرض للإنترنت | غير محدد | حوالي 35 مثيلًا (FOFA) |
الثغرة حقيقية والترقية إلى 2.8.0 هي القرار الصحيح. لكن النشرة تخطئ في تحديد الفئة، ودرجة الخطورة لا تعكس ما هو مطلوب فعليًا لاستغلال هذه الثغرة.