Apache Flink directory traversal attack: remote file writing through the REST API
Description
Apache Flink 1.5.1–1.11.2 allows arbitrary file writing via path traversal in the REST file upload handler.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Flink 1.5.1–1.11.2 allows arbitrary file writing via path traversal in the REST file upload handler.
Vulnerability
CVE-2020-17518 is an arbitrary file write vulnerability in Apache Flink's REST API, introduced in version 1.5.1 and present in all versions up to 1.11.2. The vulnerability resides in the file upload handler, which does not properly sanitize user-controlled HTTP headers. An attacker can manipulate an HTTP header to specify an arbitrary file path, enabling a directory traversal attack that writes uploaded file content to any location on the local file system accessible by the Flink process [1][3].
Exploitation
Exploitation requires network access to the Flink JobManager's REST interface (typically on port 8081). No authentication is needed for the file upload endpoint. By crafting a malicious HTTP header (likely the Content-Disposition header, based on the fix commit), an attacker can set the filename to include path traversal sequences (e.g., ../../../tmp/evil.sh), causing the server to write the uploaded file to an attacker-chosen location [2][3].
Impact
Successful exploitation allows an attacker to write arbitrary files, such as scripts, JAR files, or configuration files, to any writable directory on the Flink server's filesystem. This could lead to remote code execution if the written file is later executed (e.g., a cron job or application deployment), or to privilege escalation and persistent compromise of the Flink cluster [1][3].
Mitigation
Apache Flink released fixes in versions 1.11.3 and 1.12.0. All users with exposed Flink instances should upgrade immediately. The issue was fixed in commit a5264a6f41524afe8ceadf1d8ddc8c80f323ebc4 on the master branch [2][3]. No workarounds are documented; restricting access to the REST endpoint via network segmentation is a partial defense until patching is possible.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.flink:flink-runtimeMaven | >= 1.5.1, < 1.11.3 | 1.11.3 |
Affected products
3- osv-coords2 versions
>= 1.5.1, < 1.11.3+ 1 more
- (no CPE)range: >= 1.5.1, < 1.11.3
- (no CPE)range: >= 1.5.1, < 1.11.3
- Apache Software Foundation/Apache Flinkv5Range: Apache Flink 1.5.1 to 1.11.2
Patches
1a5264a6f4152[hotfix][runtime] A customized filename can be specified through Content-Disposition that also allows passing of path information which was not properly handled. This is fixed now.
3 files changed · +257 −121
flink-runtime/src/main/java/org/apache/flink/runtime/rest/FileUploadHandler.java+5 −2 modified@@ -51,6 +51,7 @@ import javax.annotation.Nullable; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -138,9 +139,11 @@ protected void channelRead0(final ChannelHandlerContext ctx, final HttpObject ms final DiskFileUpload fileUpload = (DiskFileUpload) data; checkState(fileUpload.isCompleted()); - final Path dest = currentUploadDir.resolve(fileUpload.getFilename()); + // wrapping around another File instantiation is a simple way to remove any path information - we're + // solely interested in the filename + final Path dest = currentUploadDir.resolve(new File(fileUpload.getFilename()).getName()); fileUpload.renameTo(dest.toFile()); - LOG.trace("Upload of file {} complete.", fileUpload.getFilename()); + LOG.trace("Upload of file {} into destination {} complete.", fileUpload.getFilename(), dest.toString()); } else if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) { final Attribute request = (Attribute) data; // this could also be implemented by using the first found Attribute as the payload
flink-runtime/src/test/java/org/apache/flink/runtime/rest/FileUploadHandlerTest.java+151 −29 modified@@ -19,9 +19,15 @@ package org.apache.flink.runtime.rest; import org.apache.flink.runtime.io.network.netty.NettyLeakDetectionResource; +import org.apache.flink.runtime.rest.handler.HandlerRequest; +import org.apache.flink.runtime.rest.messages.MessageHeaders; +import org.apache.flink.runtime.rest.messages.MessageParameters; +import org.apache.flink.runtime.rest.messages.RequestBody; import org.apache.flink.runtime.rest.util.RestMapperUtils; +import org.apache.flink.runtime.webmonitor.RestfulGateway; import org.apache.flink.util.FileUtils; import org.apache.flink.util.TestLogger; +import org.apache.flink.util.function.BiConsumerWithException; import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.ObjectMapper; import org.apache.flink.shaded.netty4.io.netty.handler.codec.http.HttpResponseStatus; @@ -31,17 +37,27 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import org.junit.After; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.lang.reflect.Field; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -52,70 +68,74 @@ */ public class FileUploadHandlerTest extends TestLogger { - @ClassRule - public static final MultipartUploadResource MULTIPART_UPLOAD_RESOURCE = new MultipartUploadResource(); + @Rule + public final MultipartUploadResource multipartUpdateResource = new MultipartUploadResource(); private static final ObjectMapper OBJECT_MAPPER = RestMapperUtils.getStrictObjectMapper(); @ClassRule public static final NettyLeakDetectionResource LEAK_DETECTION = new NettyLeakDetectionResource(); - @After - public void reset() { - MULTIPART_UPLOAD_RESOURCE.resetState(); - } - - private static Request buildMalformedRequest(String headerUrl) { + private Request buildMalformedRequest(String headerUrl) { MultipartBody.Builder builder = new MultipartBody.Builder(); builder = addFilePart(builder); // this causes a failure in the FileUploadHandler since the request should only contain form-data builder = builder.addPart(okhttp3.RequestBody.create(MediaType.parse("text/plain"), "crash")); return finalizeRequest(builder, headerUrl); } - private static Request buildMixedRequestWithUnknownAttribute(String headerUrl) throws IOException { + private Request buildMixedRequestWithUnknownAttribute(String headerUrl) throws IOException { MultipartBody.Builder builder = new MultipartBody.Builder(); builder = addJsonPart(builder, new MultipartUploadResource.TestRequestBody(), "hello"); builder = addFilePart(builder); return finalizeRequest(builder, headerUrl); } - private static Request buildFileRequest(String headerUrl) { + private Request buildRequestWithCustomFilenames(String headerUrl, String filename1, String filename2) { + MultipartBody.Builder builder = new MultipartBody.Builder(); + builder = addFilePart(builder, multipartUpdateResource.file1, filename1); + builder = addFilePart(builder, multipartUpdateResource.file2, filename2); + return finalizeRequest(builder, headerUrl); + } + + private Request buildFileRequest(String headerUrl) { MultipartBody.Builder builder = new MultipartBody.Builder(); builder = addFilePart(builder); return finalizeRequest(builder, headerUrl); } - private static Request buildJsonRequest(String headerUrl, MultipartUploadResource.TestRequestBody json) throws IOException { + private Request buildJsonRequest(String headerUrl, MultipartUploadResource.TestRequestBody json) throws IOException { MultipartBody.Builder builder = new MultipartBody.Builder(); builder = addJsonPart(builder, json, FileUploadHandler.HTTP_ATTRIBUTE_REQUEST); return finalizeRequest(builder, headerUrl); } - private static Request buildMixedRequest(String headerUrl, MultipartUploadResource.TestRequestBody json) throws IOException { + private Request buildMixedRequest(String headerUrl, MultipartUploadResource.TestRequestBody json) throws IOException { MultipartBody.Builder builder = new MultipartBody.Builder(); builder = addJsonPart(builder, json, FileUploadHandler.HTTP_ATTRIBUTE_REQUEST); builder = addFilePart(builder); return finalizeRequest(builder, headerUrl); } - private static Request finalizeRequest(MultipartBody.Builder builder, String headerUrl) { + private Request finalizeRequest(MultipartBody.Builder builder, String headerUrl) { MultipartBody multipartBody = builder .setType(MultipartBody.FORM) .build(); return new Request.Builder() - .url(MULTIPART_UPLOAD_RESOURCE.serverAddress + headerUrl) + .url(multipartUpdateResource.serverAddress + headerUrl) .post(multipartBody) .build(); } - private static MultipartBody.Builder addFilePart(MultipartBody.Builder builder) { - for (File file : MULTIPART_UPLOAD_RESOURCE.getFilesToUpload()) { - okhttp3.RequestBody filePayload = okhttp3.RequestBody.create(MediaType.parse("application/octet-stream"), file); + private MultipartBody.Builder addFilePart(final MultipartBody.Builder builder) { + multipartUpdateResource.getFilesToUpload().forEach(f -> addFilePart(builder, f, f.getName())); + return builder; + } - builder = builder.addFormDataPart(file.getName(), file.getName(), filePayload); - } + private static MultipartBody.Builder addFilePart(MultipartBody.Builder builder, File file, String filename) { + okhttp3.RequestBody filePayload = okhttp3.RequestBody.create(MediaType.parse("application/octet-stream"), file); + builder = builder.addFormDataPart(file.getName(), filename, filePayload); return builder; } @@ -133,9 +153,9 @@ private static MultipartBody.Builder addJsonPart(MultipartBody.Builder builder, public void testUploadDirectoryRegeneration() throws Exception { OkHttpClient client = createOkHttpClientWithNoTimeouts(); - MultipartUploadResource.MultipartFileHandler fileHandler = MULTIPART_UPLOAD_RESOURCE.getFileHandler(); + MultipartUploadResource.MultipartFileHandler fileHandler = multipartUpdateResource.getFileHandler(); - FileUtils.deleteDirectory(MULTIPART_UPLOAD_RESOURCE.getUploadDirectory().toFile()); + FileUtils.deleteDirectory(multipartUpdateResource.getUploadDirectory().toFile()); Request fileRequest = buildFileRequest(fileHandler.getMessageHeaders().getTargetRestEndpointURL()); try (Response response = client.newCall(fileRequest).execute()) { @@ -149,7 +169,7 @@ public void testUploadDirectoryRegeneration() throws Exception { public void testMixedMultipart() throws Exception { OkHttpClient client = createOkHttpClientWithNoTimeouts(); - MultipartUploadResource.MultipartMixedHandler mixedHandler = MULTIPART_UPLOAD_RESOURCE.getMixedHandler(); + MultipartUploadResource.MultipartMixedHandler mixedHandler = multipartUpdateResource.getMixedHandler(); Request jsonRequest = buildJsonRequest(mixedHandler.getMessageHeaders().getTargetRestEndpointURL(), new MultipartUploadResource.TestRequestBody()); try (Response response = client.newCall(jsonRequest).execute()) { @@ -177,7 +197,7 @@ public void testMixedMultipart() throws Exception { public void testJsonMultipart() throws Exception { OkHttpClient client = createOkHttpClientWithNoTimeouts(); - MultipartUploadResource.MultipartJsonHandler jsonHandler = MULTIPART_UPLOAD_RESOURCE.getJsonHandler(); + MultipartUploadResource.MultipartJsonHandler jsonHandler = multipartUpdateResource.getJsonHandler(); MultipartUploadResource.TestRequestBody json = new MultipartUploadResource.TestRequestBody(); Request jsonRequest = buildJsonRequest(jsonHandler.getMessageHeaders().getTargetRestEndpointURL(), json); @@ -205,7 +225,7 @@ public void testJsonMultipart() throws Exception { public void testFileMultipart() throws Exception { OkHttpClient client = createOkHttpClientWithNoTimeouts(); - MultipartUploadResource.MultipartFileHandler fileHandler = MULTIPART_UPLOAD_RESOURCE.getFileHandler(); + MultipartUploadResource.MultipartFileHandler fileHandler = multipartUpdateResource.getFileHandler(); Request jsonRequest = buildJsonRequest(fileHandler.getMessageHeaders().getTargetRestEndpointURL(), new MultipartUploadResource.TestRequestBody()); try (Response response = client.newCall(jsonRequest).execute()) { @@ -231,11 +251,12 @@ public void testFileMultipart() throws Exception { public void testUploadCleanupOnUnknownAttribute() throws IOException { OkHttpClient client = createOkHttpClientWithNoTimeouts(); - Request request = buildMixedRequestWithUnknownAttribute(MULTIPART_UPLOAD_RESOURCE.getMixedHandler().getMessageHeaders().getTargetRestEndpointURL()); + Request request = buildMixedRequestWithUnknownAttribute(multipartUpdateResource + .getMixedHandler().getMessageHeaders().getTargetRestEndpointURL()); try (Response response = client.newCall(request).execute()) { assertEquals(HttpResponseStatus.BAD_REQUEST.code(), response.code()); } - MULTIPART_UPLOAD_RESOURCE.assertUploadDirectoryIsEmpty(); + multipartUpdateResource.assertUploadDirectoryIsEmpty(); verifyNoFileIsRegisteredToDeleteOnExitHook(); } @@ -247,16 +268,117 @@ public void testUploadCleanupOnUnknownAttribute() throws IOException { public void testUploadCleanupOnFailure() throws IOException { OkHttpClient client = createOkHttpClientWithNoTimeouts(); - Request request = buildMalformedRequest(MULTIPART_UPLOAD_RESOURCE.getMixedHandler().getMessageHeaders().getTargetRestEndpointURL()); + Request request = buildMalformedRequest(multipartUpdateResource + .getMixedHandler().getMessageHeaders().getTargetRestEndpointURL()); try (Response response = client.newCall(request).execute()) { // decoding errors aren't handled separately by the FileUploadHandler assertEquals(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), response.code()); } - MULTIPART_UPLOAD_RESOURCE.assertUploadDirectoryIsEmpty(); + multipartUpdateResource.assertUploadDirectoryIsEmpty(); + + verifyNoFileIsRegisteredToDeleteOnExitHook(); + } + + @Test + public void testFileUploadUsingCustomFilename() throws IOException { + OkHttpClient client = createOkHttpClientWithNoTimeouts(); + + String customFilename1 = "different-name-1.jar"; + String customFilename2 = "different-name-2.jar"; + + multipartUpdateResource.setFileUploadVerifier(new CustomFilenameVerifier( + customFilename1, + multipartUpdateResource.file1.toPath(), + customFilename2, + multipartUpdateResource.file2.toPath())); + + MessageHeaders<?, ?, ?> messageHeaders = multipartUpdateResource.getFileHandler().getMessageHeaders(); + Request request = buildRequestWithCustomFilenames( + messageHeaders.getTargetRestEndpointURL(), + customFilename1, + customFilename2); + try (Response response = client.newCall(request).execute()) { + assertEquals(messageHeaders.getResponseStatusCode().code(), response.code()); + } + + verifyNoFileIsRegisteredToDeleteOnExitHook(); + } + + @Test + public void testFileUploadUsingCustomFilenameWithParentFolderPath() throws IOException { + OkHttpClient client = createOkHttpClientWithNoTimeouts(); + + String customFilename1 = "different-name-1.jar"; + String customFilename2 = "different-name-2.jar"; + + multipartUpdateResource.setFileUploadVerifier(new CustomFilenameVerifier( + customFilename1, + multipartUpdateResource.file1.toPath(), + customFilename2, + multipartUpdateResource.file2.toPath())); + + // referring to the parent folder within the filename should be ignored + MessageHeaders<?, ?, ?> messageHeaders = multipartUpdateResource.getFileHandler().getMessageHeaders(); + Request request = buildRequestWithCustomFilenames( + multipartUpdateResource.getFileHandler().getMessageHeaders().getTargetRestEndpointURL(), + String.format("../%s", customFilename1), + String.format("../%s", customFilename2)); + try (Response response = client.newCall(request).execute()) { + assertEquals(messageHeaders.getResponseStatusCode().code(), response.code()); + } verifyNoFileIsRegisteredToDeleteOnExitHook(); } + private static class CustomFilenameVerifier implements BiConsumerWithException<HandlerRequest<? extends RequestBody, ? extends MessageParameters>, RestfulGateway, Exception> { + + private final String customFilename1; + private final Path fileContent1; + + private final String customFilename2; + private final Path fileContent2; + + public CustomFilenameVerifier(String customFilename1, Path fileContent1, String customFilename2, Path fileContent2) { + this.customFilename1 = customFilename1; + this.fileContent1 = fileContent1; + + this.customFilename2 = customFilename2; + this.fileContent2 = fileContent2; + } + + @Override + public void accept( + HandlerRequest<? extends RequestBody, ? extends MessageParameters> request, + RestfulGateway restfulGateway) throws Exception { + List<Path> uploadedFiles = request.getUploadedFiles().stream().map(File::toPath).collect( + Collectors.toList()); + + List<Path> actualList = new ArrayList<>(uploadedFiles); + actualList.sort(Comparator.comparing(Path::toString)); + + SortedMap<String, Path> expectedFilenamesAndContent = new TreeMap<>(); + expectedFilenamesAndContent.put(customFilename1, fileContent1); + expectedFilenamesAndContent.put(customFilename2, fileContent2); + + assertEquals(expectedFilenamesAndContent.size(), uploadedFiles.size()); + + Iterator<Path> uploadedFileIterator = actualList.iterator(); + for (Map.Entry<String, Path> expectedFilenameAndContent : expectedFilenamesAndContent.entrySet()) { + String expectedFilename = expectedFilenameAndContent.getKey(); + Path expectedContent = expectedFilenameAndContent.getValue(); + + assertTrue(uploadedFileIterator.hasNext()); + Path actual = uploadedFileIterator.next(); + + assertEquals(expectedFilename, actual.getFileName().toString()); + + byte[] originalContent = java.nio.file.Files.readAllBytes(expectedContent); + byte[] receivedContent = java.nio.file.Files.readAllBytes(actual); + assertArrayEquals(originalContent, receivedContent); + } + } + } + private OkHttpClient createOkHttpClientWithNoTimeouts() { // don't fail if some OkHttpClient operations take longer. See FLINK-17725 return new OkHttpClient.Builder()
flink-runtime/src/test/java/org/apache/flink/runtime/rest/MultipartUploadResource.java+101 −90 modified@@ -29,12 +29,14 @@ import org.apache.flink.runtime.rest.messages.EmptyRequestBody; import org.apache.flink.runtime.rest.messages.EmptyResponseBody; import org.apache.flink.runtime.rest.messages.MessageHeaders; +import org.apache.flink.runtime.rest.messages.MessageParameters; import org.apache.flink.runtime.rest.messages.RequestBody; import org.apache.flink.runtime.rest.util.TestRestServerEndpoint; import org.apache.flink.runtime.rpc.RpcUtils; import org.apache.flink.runtime.webmonitor.RestfulGateway; import org.apache.flink.runtime.webmonitor.retriever.GatewayRetriever; import org.apache.flink.util.Preconditions; +import org.apache.flink.util.function.BiConsumerWithException; import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.annotation.JsonCreator; import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.annotation.JsonProperty; @@ -90,6 +92,8 @@ public class MultipartUploadResource extends ExternalResource { private Path configuredUploadDir; + private BiConsumerWithException<HandlerRequest<?, ?>, RestfulGateway, RestHandlerException> fileUploadVerifier; + @Override public void before() throws Exception { temporaryFolder.create(); @@ -115,9 +119,9 @@ public void before() throws Exception { file2 = temporaryFolder.newFile(); Files.write(file2.toPath(), "world".getBytes(ConfigConstants.DEFAULT_CHARSET)); - mixedHandler = new MultipartMixedHandler(mockGatewayRetriever, Arrays.asList(file1.toPath(), file2.toPath())); + mixedHandler = new MultipartMixedHandler(mockGatewayRetriever); jsonHandler = new MultipartJsonHandler(mockGatewayRetriever); - fileHandler = new MultipartFileHandler(mockGatewayRetriever, Arrays.asList(file1.toPath(), file2.toPath())); + fileHandler = new MultipartFileHandler(mockGatewayRetriever); serverEndpoint = TestRestServerEndpoint.builder(serverConfig) .withHandler(mixedHandler) @@ -127,6 +131,41 @@ public void before() throws Exception { serverAddress = serverEndpoint.getRestBaseUrl(); serverSocketAddress = serverEndpoint.getServerAddress(); + + this.setFileUploadVerifier((request, restfulGateway) -> { + // the default verifier checks for identiy (i.e. same name and content) of all uploaded files + List<Path> expectedFiles = Arrays.asList(file1.toPath(), file2.toPath()); + List<Path> uploadedFiles = request.getUploadedFiles().stream().map(File::toPath).collect(Collectors.toList()); + + assertEquals(expectedFiles.size(), uploadedFiles.size()); + + List<Path> expectedList = new ArrayList<>(expectedFiles); + List<Path> actualList = new ArrayList<>(uploadedFiles); + expectedList.sort(Comparator.comparing(Path::toString)); + actualList.sort(Comparator.comparing(Path::toString)); + + for (int x = 0; x < expectedList.size(); x++) { + Path expected = expectedList.get(x); + Path actual = actualList.get(x); + + assertEquals(expected.getFileName().toString(), actual.getFileName().toString()); + + byte[] originalContent = Files.readAllBytes(expected); + byte[] receivedContent = Files.readAllBytes(actual); + assertArrayEquals(originalContent, receivedContent); + } + }); + } + + public void setFileUploadVerifier(BiConsumerWithException<HandlerRequest<? extends RequestBody, ? extends MessageParameters>, RestfulGateway, Exception> verifier) { + this.fileUploadVerifier = (request, restfulGateway) -> { + try { + verifier.accept(request, restfulGateway); + } catch (Exception e) { + // return 505 to differentiate from common BAD_REQUEST responses in this test + throw new RestHandlerException("Test verification failed.", HttpResponseStatus.HTTP_VERSION_NOT_SUPPORTED, e); + } + }; } public Collection<File> getFilesToUpload() { @@ -192,67 +231,67 @@ public void assertUploadDirectoryIsEmpty() throws IOException { /** * Handler that accepts a mixed request consisting of a {@link TestRequestBody} and {@link #file1} and {@link #file2}. */ - public static class MultipartMixedHandler extends AbstractRestHandler<RestfulGateway, TestRequestBody, EmptyResponseBody, EmptyMessageParameters> { - private final Collection<Path> expectedFiles; + public class MultipartMixedHandler extends AbstractRestHandler<RestfulGateway, TestRequestBody, EmptyResponseBody, EmptyMessageParameters> { + volatile TestRequestBody lastReceivedRequest = null; - MultipartMixedHandler(GatewayRetriever<RestfulGateway> leaderRetriever, Collection<Path> expectedFiles) { + MultipartMixedHandler(GatewayRetriever<RestfulGateway> leaderRetriever) { super(leaderRetriever, RpcUtils.INF_TIMEOUT, Collections.emptyMap(), MultipartMixedHeaders.INSTANCE); - this.expectedFiles = expectedFiles; } @Override protected CompletableFuture<EmptyResponseBody> handleRequest(@Nonnull HandlerRequest<TestRequestBody, EmptyMessageParameters> request, @Nonnull RestfulGateway gateway) throws RestHandlerException { - MultipartFileHandler.verifyFileUpload(expectedFiles, request.getUploadedFiles().stream().map(File::toPath).collect(Collectors.toList())); + MultipartUploadResource.this.fileUploadVerifier.accept(request, gateway); this.lastReceivedRequest = request.getRequestBody(); return CompletableFuture.completedFuture(EmptyResponseBody.getInstance()); } - private static final class MultipartMixedHeaders implements MessageHeaders<TestRequestBody, EmptyResponseBody, EmptyMessageParameters> { - private static final MultipartMixedHeaders INSTANCE = new MultipartMixedHeaders(); + } - private MultipartMixedHeaders() { - } + private static final class MultipartMixedHeaders implements MessageHeaders<TestRequestBody, EmptyResponseBody, EmptyMessageParameters> { + private static final MultipartMixedHeaders INSTANCE = new MultipartMixedHeaders(); - @Override - public Class<TestRequestBody> getRequestClass() { - return TestRequestBody.class; - } + private MultipartMixedHeaders() { + } - @Override - public Class<EmptyResponseBody> getResponseClass() { - return EmptyResponseBody.class; - } + @Override + public Class<TestRequestBody> getRequestClass() { + return TestRequestBody.class; + } - @Override - public HttpResponseStatus getResponseStatusCode() { - return HttpResponseStatus.OK; - } + @Override + public Class<EmptyResponseBody> getResponseClass() { + return EmptyResponseBody.class; + } - @Override - public String getDescription() { - return ""; - } + @Override + public HttpResponseStatus getResponseStatusCode() { + return HttpResponseStatus.OK; + } - @Override - public EmptyMessageParameters getUnresolvedMessageParameters() { - return EmptyMessageParameters.getInstance(); - } + @Override + public String getDescription() { + return ""; + } - @Override - public HttpMethodWrapper getHttpMethod() { - return HttpMethodWrapper.POST; - } + @Override + public EmptyMessageParameters getUnresolvedMessageParameters() { + return EmptyMessageParameters.getInstance(); + } - @Override - public String getTargetRestEndpointURL() { - return "/test/upload/mixed"; - } + @Override + public HttpMethodWrapper getHttpMethod() { + return HttpMethodWrapper.POST; + } - @Override - public boolean acceptsFileUploads() { - return true; - } + @Override + public String getTargetRestEndpointURL() { + return "/test/upload/mixed"; + } + + @Override + public boolean acceptsFileUploads() { + return true; } } @@ -300,68 +339,40 @@ public boolean acceptsFileUploads() { } /** - * Handler that accepts a file request consisting of and {@link #file1} and {@link #file2}. + * Handler that accepts a file request and calls {@link MultipartUploadResource#fileUploadVerifier} to verify it. */ - public static class MultipartFileHandler extends AbstractRestHandler<RestfulGateway, EmptyRequestBody, EmptyResponseBody, EmptyMessageParameters> { - - private final Collection<Path> expectedFiles; + public class MultipartFileHandler extends AbstractRestHandler<RestfulGateway, EmptyRequestBody, EmptyResponseBody, EmptyMessageParameters> { - MultipartFileHandler(GatewayRetriever<RestfulGateway> leaderRetriever, Collection<Path> expectedFiles) { + MultipartFileHandler(GatewayRetriever<RestfulGateway> leaderRetriever) { super(leaderRetriever, RpcUtils.INF_TIMEOUT, Collections.emptyMap(), MultipartFileHeaders.INSTANCE); - this.expectedFiles = expectedFiles; } @Override protected CompletableFuture<EmptyResponseBody> handleRequest(@Nonnull HandlerRequest<EmptyRequestBody, EmptyMessageParameters> request, @Nonnull RestfulGateway gateway) throws RestHandlerException { - verifyFileUpload(expectedFiles, request.getUploadedFiles().stream().map(File::toPath).collect(Collectors.toList())); + MultipartUploadResource.this.fileUploadVerifier.accept(request, gateway); return CompletableFuture.completedFuture(EmptyResponseBody.getInstance()); } + } - static void verifyFileUpload(Collection<Path> expectedFiles, Collection<Path> uploadedFiles) throws RestHandlerException { - try { - assertEquals(expectedFiles.size(), uploadedFiles.size()); - - List<Path> expectedList = new ArrayList<>(expectedFiles); - List<Path> actualList = new ArrayList<>(uploadedFiles); - expectedList.sort(Comparator.comparing(Path::toString)); - actualList.sort(Comparator.comparing(Path::toString)); - - for (int x = 0; x < expectedList.size(); x++) { - Path expected = expectedList.get(x); - Path actual = actualList.get(x); - - assertEquals(expected.getFileName().toString(), actual.getFileName().toString()); + private static class MultipartFileHeaders extends TestHeadersBase<EmptyRequestBody> { + static final MultipartFileHeaders INSTANCE = new MultipartFileHeaders(); - byte[] originalContent = Files.readAllBytes(expected); - byte[] receivedContent = Files.readAllBytes(actual); - assertArrayEquals(originalContent, receivedContent); - } - } catch (Exception e) { - // return 505 to differentiate from common BAD_REQUEST responses in this test - throw new RestHandlerException("Test verification failed.", HttpResponseStatus.HTTP_VERSION_NOT_SUPPORTED, e); - } + private MultipartFileHeaders() { } - private static final class MultipartFileHeaders extends TestHeadersBase<EmptyRequestBody> { - private static final MultipartFileHeaders INSTANCE = new MultipartFileHeaders(); - - private MultipartFileHeaders() { - } - - @Override - public Class<EmptyRequestBody> getRequestClass() { - return EmptyRequestBody.class; - } + @Override + public Class<EmptyRequestBody> getRequestClass() { + return EmptyRequestBody.class; + } - @Override - public String getTargetRestEndpointURL() { - return "/test/upload/file"; - } + @Override + public String getTargetRestEndpointURL() { + return "/test/upload/file"; + } - @Override - public boolean acceptsFileUploads() { - return true; - } + @Override + public boolean acceptsFileUploads() { + return true; } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
50- github.com/advisories/GHSA-7q5g-gph2-4rc6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-17518ghsaADVISORY
- www.openwall.com/lists/oss-security/2021/01/05/1ghsamailing-listx_refsource_MLISTWEB
- github.com/apache/flink/commit/a5264a6f41524afe8ceadf1d8ddc8c80f323ebc4ghsaWEB
- lists.apache.org/thread.html/r0b000dc028616d33cb9aa388eb45d516b789cab0024dad94bc06588a%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r0b000dc028616d33cb9aa388eb45d516b789cab0024dad94bc06588a@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r1125f3044a0946d1e7e6f125a6170b58d413ebd4a95157e4608041c7%40%3Cannounce.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r1125f3044a0946d1e7e6f125a6170b58d413ebd4a95157e4608041c7@%3Cannounce.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r229167538863518738e02f4c1c5a8bb34c1d45dadcc97adf6676b0c1%40%3Cdev.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r229167538863518738e02f4c1c5a8bb34c1d45dadcc97adf6676b0c1@%3Cdev.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r26fcdd4fe288323006253437ebc4dd6fdfadfb5e93465a0e4f68420d%40%3Cuser-zh.flink.apache.org%3Emitrex_refsource_MISC
- lists.apache.org/thread.html/r26fcdd4fe288323006253437ebc4dd6fdfadfb5e93465a0e4f68420d@%3Cuser-zh.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r28f17e564950d663e68cc6fe75756012dda62ac623766bb9bc5e7034%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r28f17e564950d663e68cc6fe75756012dda62ac623766bb9bc5e7034@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r4a87837518804b31eb9db3048347ed2bb7b46fbaad5844f22a9fd4dc%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r4a87837518804b31eb9db3048347ed2bb7b46fbaad5844f22a9fd4dc@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r5444acac3407ef6397d6aef1b5aec2db53b4b88ef221e63084c1e5f2%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r5444acac3407ef6397d6aef1b5aec2db53b4b88ef221e63084c1e5f2@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r705fb2211b82c9f1f8d2b1d4c823bcbca50402ba09b96608ec657efe%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r705fb2211b82c9f1f8d2b1d4c823bcbca50402ba09b96608ec657efe@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r710693b0d3b229c81f485804ea1145b4edda79c9e77d66c39a0a2ff1%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r710693b0d3b229c81f485804ea1145b4edda79c9e77d66c39a0a2ff1@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r7b2ee88c66fc1d0823e66475631f5c3e7f0365204ff0cb094d9f2433%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r7b2ee88c66fc1d0823e66475631f5c3e7f0365204ff0cb094d9f2433@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r8167f30c4c60a11b8d5be3f55537beeda629be61196e693bde403b36%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r8167f30c4c60a11b8d5be3f55537beeda629be61196e693bde403b36@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r88200d2f0b620c6b4b1585a7171355005c89e678b01d0e71a16c57e7%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r88200d2f0b620c6b4b1585a7171355005c89e678b01d0e71a16c57e7@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/r90890afea72a9571d666820b2fe5942a0a5f86be406fa31da3dd0922%40%3Cannounce.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/r90890afea72a9571d666820b2fe5942a0a5f86be406fa31da3dd0922@%3Cannounce.apache.org%3EghsaWEB
- lists.apache.org/thread.html/ra8c96bf3ccb4e491f9ce87ba35f134b4449beb2a38d1ce28fd89001f%40%3Cdev.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/ra8c96bf3ccb4e491f9ce87ba35f134b4449beb2a38d1ce28fd89001f@%3Cdev.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rb43cd476419a48be89c1339b527a18116f23eec5b6df2b2acbfef261%40%3Cannounce.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/rb43cd476419a48be89c1339b527a18116f23eec5b6df2b2acbfef261%40%3Cdev.flink.apache.org%3Eghsax_refsource_MISCmailing-listx_refsource_MLISTWEB
- lists.apache.org/thread.html/rb43cd476419a48be89c1339b527a18116f23eec5b6df2b2acbfef261%40%3Cuser.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/rb43cd476419a48be89c1339b527a18116f23eec5b6df2b2acbfef261@%3Cannounce.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rb43cd476419a48be89c1339b527a18116f23eec5b6df2b2acbfef261@%3Cdev.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rb43cd476419a48be89c1339b527a18116f23eec5b6df2b2acbfef261@%3Cuser.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rcb9e8af775f2a3706b69153aefde78f208871649df057c70ce2e24f9%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/rcb9e8af775f2a3706b69153aefde78f208871649df057c70ce2e24f9@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rd2467344f88bcaf108b8209ca92da8ec393c68174bfb8c27d1e20faa%40%3Cdev.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/rd2467344f88bcaf108b8209ca92da8ec393c68174bfb8c27d1e20faa@%3Cdev.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rd6a1a0e2d73220a65a8f6535bbcd24bb66adb0d046c4a1aa18777cf3%40%3Cdev.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/rd6a1a0e2d73220a65a8f6535bbcd24bb66adb0d046c4a1aa18777cf3@%3Cdev.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rec0d650fbd4ea1a5e1224a347d83a63cb44291c334ad58b8809bc23b%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/rec0d650fbd4ea1a5e1224a347d83a63cb44291c334ad58b8809bc23b@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rf8812a5703f4a5f1341138baf239258b250875699732cfdf9d55b21d%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/rf8812a5703f4a5f1341138baf239258b250875699732cfdf9d55b21d@%3Cissues.flink.apache.org%3EghsaWEB
- lists.apache.org/thread.html/rfe159ccf496d75813f24c6079c5d33872d83f5a2e39cb32c3aef5a73%40%3Cissues.flink.apache.org%3Emitremailing-listx_refsource_MLIST
- lists.apache.org/thread.html/rfe159ccf496d75813f24c6079c5d33872d83f5a2e39cb32c3aef5a73@%3Cissues.flink.apache.org%3EghsaWEB
News mentions
0No linked articles in our index yet.