CVE-2021-22118
Description
In Spring Framework, versions 5.2.x prior to 5.2.15 and versions 5.3.x prior to 5.3.7, a WebFlux application is vulnerable to a privilege escalation: by (re)creating the temporary storage directory, a locally authenticated malicious user can read or modify files that have been uploaded to the WebFlux application, or overwrite arbitrary files with multipart request data.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
In Spring Framework WebFlux, a locally authenticated attacker can escalate privileges by recreating the temporary storage directory for multipart file uploads.
Vulnerability
In Spring Framework versions 5.2.x prior to 5.2.15 and 5.3.x prior to 5.3.7, the WebFlux module uses a predictable temporary directory for storing multipart file uploads. The vulnerability lies in both SynchronossPartHttpMessageReader and DefaultPartHttpMessageReader classes, which create a temporary storage directory without sufficient uniqueness guarantees [1][2][3]. A locally authenticated attacker can recreate this directory, enabling unauthorized access to uploaded files.
Exploitation
An attacker with local authentication and the ability to create or recreate directories on the filesystem can exploit this flaw. By (re)creating the temporary storage directory used by the WebFlux application before or during file upload processing, the attacker can intercept, read, modify, or overwrite files that are stored temporarily during multipart requests [1]. No network position beyond local access is required; the attacker must be able to predict or determine the directory name and have write access to the location.
Impact
Successful exploitation allows the attacker to read or modify files uploaded to the WebFlux application, as well as overwrite arbitrary files with multipart request data, leading to privilege escalation [1]. This can result in disclosure of sensitive information, file tampering, or arbitrary file write depending on the application's file handling.
Mitigation
The vulnerability is fixed in Spring Framework versions 5.2.15 and 5.3.7 [1]. The patches introduce unique temporary directory names via FileStorage abstraction and configurable file storage directories (see commits [2][3]). Users should upgrade to these versions or later. There is no known workaround for unpatched versions; the fix ensures directory names are not predictable and cannot be recreated by an attacker.
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.springframework:spring-webMaven | >= 5.2.0, < 5.2.15 | 5.2.15 |
org.springframework:spring-webMaven | >= 5.3.0, < 5.3.7 | 5.3.7 |
Affected products
2- Spring/Spring Frameworkdescription
Patches
20d0d75e25322Ensure DefaultPartHttpMessageReader temp directories do not collide
4 files changed · +217 −25
spring-web/src/main/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.java+7 −22 modified@@ -19,9 +19,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; @@ -63,8 +61,6 @@ */ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader<Part> { - private static final String IDENTIFIER = "spring-multipart"; - private int maxInMemorySize = 256 * 1024; private int maxHeadersSize = 8 * 1024; @@ -77,7 +73,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements private Scheduler blockingOperationScheduler = Schedulers.boundedElastic(); - private Mono<Path> fileStorageDirectory = Mono.defer(this::defaultFileStorageDirectory).cache(); + private FileStorage fileStorage = FileStorage.tempDirectory(this::getBlockingOperationScheduler); private Charset headersCharset = StandardCharsets.UTF_8; @@ -147,10 +143,7 @@ public void setMaxParts(int maxParts) { */ public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); - if (!Files.exists(fileStorageDirectory)) { - Files.createDirectory(fileStorageDirectory); - } - this.fileStorageDirectory = Mono.just(fileStorageDirectory); + this.fileStorage = FileStorage.fromPath(fileStorageDirectory); } /** @@ -168,6 +161,10 @@ public void setBlockingOperationScheduler(Scheduler blockingOperationScheduler) this.blockingOperationScheduler = blockingOperationScheduler; } + private Scheduler getBlockingOperationScheduler() { + return this.blockingOperationScheduler; + } + /** * When set to {@code true}, the {@linkplain Part#content() part content} * is streamed directly from the parsed input buffer stream, and not stored @@ -230,7 +227,7 @@ public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage mess this.maxHeadersSize, this.headersCharset); return PartGenerator.createParts(tokens, this.maxParts, this.maxInMemorySize, this.maxDiskUsagePerPart, - this.streaming, this.fileStorageDirectory, this.blockingOperationScheduler); + this.streaming, this.fileStorage.directory(), this.blockingOperationScheduler); }); } @@ -250,16 +247,4 @@ private byte[] boundary(HttpMessage message) { return null; } - @SuppressWarnings("BlockingMethodInNonBlockingContext") - private Mono<Path> defaultFileStorageDirectory() { - return Mono.fromCallable(() -> { - Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"), IDENTIFIER); - if (!Files.exists(tempDirectory)) { - Files.createDirectory(tempDirectory); - } - return tempDirectory; - }).subscribeOn(this.blockingOperationScheduler); - - } - }
spring-web/src/main/java/org/springframework/http/codec/multipart/FileStorage.java+128 −0 added@@ -0,0 +1,128 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.codec.multipart; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +/** + * Represents a directory used to store parts larger than + * {@link DefaultPartHttpMessageReader#setMaxInMemorySize(int)}. + * + * @author Arjen Poutsma + * @since 5.3.7 + */ +abstract class FileStorage { + + private static final Log logger = LogFactory.getLog(FileStorage.class); + + + protected FileStorage() { + } + + /** + * Get the mono of the directory to store files in. + */ + public abstract Mono<Path> directory(); + + + /** + * Create a new {@code FileStorage} from a user-specified path. Creates the + * path if it does not exist. + */ + public static FileStorage fromPath(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createDirectory(path); + } + return new PathFileStorage(path); + } + + /** + * Create a new {@code FileStorage} based a on a temporary directory. + * @param scheduler scheduler to use for blocking operations + */ + public static FileStorage tempDirectory(Supplier<Scheduler> scheduler) { + return new TempFileStorage(scheduler); + } + + + private static final class PathFileStorage extends FileStorage { + + private final Mono<Path> directory; + + public PathFileStorage(Path directory) { + this.directory = Mono.just(directory); + } + + @Override + public Mono<Path> directory() { + return this.directory; + } + } + + + private static final class TempFileStorage extends FileStorage { + + private static final String IDENTIFIER = "spring-multipart-"; + + private final Supplier<Scheduler> scheduler; + + private volatile Mono<Path> directory = tempDirectory(); + + + public TempFileStorage(Supplier<Scheduler> scheduler) { + this.scheduler = scheduler; + } + + @Override + public Mono<Path> directory() { + return this.directory + .flatMap(this::createNewDirectoryIfDeleted) + .subscribeOn(this.scheduler.get()); + } + + private Mono<Path> createNewDirectoryIfDeleted(Path directory) { + if (!Files.exists(directory)) { + // Some daemons remove temp directories. Let's create a new one. + Mono<Path> newDirectory = tempDirectory(); + this.directory = newDirectory; + return newDirectory; + } + else { + return Mono.just(directory); + } + } + + private static Mono<Path> tempDirectory() { + return Mono.fromCallable(() -> { + Path directory = Files.createTempDirectory(IDENTIFIER); + if (logger.isDebugEnabled()) { + logger.debug("Created temporary storage directory: " + directory); + } + return directory; + }).cache(); + } + } + +}
spring-web/src/main/java/org/springframework/http/codec/multipart/PartGenerator.java+0 −3 modified@@ -578,9 +578,6 @@ public void createFile() { private WritingFileState createFileState(Path directory) { try { - if (!Files.exists(directory)) { - Files.createDirectory(directory); - } Path tempFile = Files.createTempFile(directory, null, ".multipart"); if (logger.isTraceEnabled()) { logger.trace("Storing multipart data in file " + tempFile);
spring-web/src/test/java/org/springframework/http/codec/multipart/FileStorageTests.java+82 −0 added@@ -0,0 +1,82 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.codec.multipart; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FileStorageTests { + + @Test + void fromPath() throws IOException { + Path path = Files.createTempFile("spring", "test"); + FileStorage storage = FileStorage.fromPath(path); + + Mono<Path> directory = storage.directory(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + } + + @Test + void tempDirectory() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono<Path> directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path -> { + assertThat(path).exists(); + StepVerifier.create(directory) + .expectNext(path) + .verifyComplete(); + }) + .verifyComplete(); + } + + @Test + void tempDirectoryDeleted() { + FileStorage storage = FileStorage.tempDirectory(Schedulers::boundedElastic); + + Mono<Path> directory = storage.directory(); + StepVerifier.create(directory) + .consumeNextWith(path1 -> { + try { + Files.delete(path1); + StepVerifier.create(directory) + .consumeNextWith(path2 -> assertThat(path2).isNotEqualTo(path1)) + .verifyComplete(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }) + .verifyComplete(); + } + +}
cce60c479c22Ensure Synchronoss temp directories do not collide
1 file changed · +38 −3
spring-web/src/main/java/org/springframework/http/codec/multipart/SynchronossPartHttpMessageReader.java+38 −3 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,13 @@ package org.springframework.http.codec.multipart; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -78,12 +80,16 @@ */ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader<Part> { + private static final String FILE_STORAGE_DIRECTORY_PREFIX = "synchronoss-file-upload-"; + private int maxInMemorySize = 256 * 1024; private long maxDiskUsagePerPart = -1; private int maxParts = -1; + private Path fileStorageDirectory = createTempDirectory(); + /** * Configure the maximum amount of memory that is allowed to use per part. @@ -144,6 +150,22 @@ public int getMaxParts() { return this.maxParts; } + /** + * Set the directory used to store parts larger than + * {@link #setMaxInMemorySize(int) maxInMemorySize}. By default, a new + * temporary directory is created. + * @throws IOException if an I/O error occurs, or the parent directory + * does not exist + * @since 5.3.7 + */ + public void setFileStorageDirectory(Path fileStorageDirectory) throws IOException { + Assert.notNull(fileStorageDirectory, "FileStorageDirectory must not be null"); + if (!Files.exists(fileStorageDirectory)) { + Files.createDirectory(fileStorageDirectory); + } + this.fileStorageDirectory = fileStorageDirectory; + } + @Override public List<MediaType> getReadableMediaTypes() { @@ -167,7 +189,7 @@ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType @Override public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) { - return Flux.create(new SynchronossPartGenerator(message)) + return Flux.create(new SynchronossPartGenerator(message, this.fileStorageDirectory)) .doOnNext(part -> { if (!Hints.isLoggingSuppressed(hints)) { LogFormatUtils.traceDebug(logger, traceOn -> Hints.getLogPrefix(hints) + "Parsed " + @@ -183,6 +205,15 @@ public Mono<Part> readMono(ResolvableType elementType, ReactiveHttpInputMessage return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part")); } + private static Path createTempDirectory() { + try { + return Files.createTempDirectory(FILE_STORAGE_DIRECTORY_PREFIX); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + /** * Subscribe to the input stream and feed the Synchronoss parser. Then listen @@ -194,14 +225,17 @@ private class SynchronossPartGenerator extends BaseSubscriber<DataBuffer> implem private final LimitedPartBodyStreamStorageFactory storageFactory = new LimitedPartBodyStreamStorageFactory(); + private final Path fileStorageDirectory; + @Nullable private NioMultipartParserListener listener; @Nullable private NioMultipartParser parser; - public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage) { + public SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, Path fileStorageDirectory) { this.inputMessage = inputMessage; + this.fileStorageDirectory = fileStorageDirectory; } @Override @@ -218,6 +252,7 @@ public void accept(FluxSink<Part> sink) { this.parser = Multipart .multipart(context) + .saveTemporaryFilesTo(this.fileStorageDirectory.toString()) .usePartBodyStreamStorageFactory(this.storageFactory) .forNIO(this.listener);
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
14- github.com/advisories/GHSA-gfwj-fwqj-fp3vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-22118ghsaADVISORY
- github.com/spring-projects/spring-framework/commit/0d0d75e25322d8161002d861fff3ec04ba8be5acghsaWEB
- github.com/spring-projects/spring-framework/commit/cce60c479c22101f24b2b4abebb6d79440b120d1ghsaWEB
- github.com/spring-projects/spring-framework/issues/26931ghsaWEB
- security.netapp.com/advisory/ntap-20210713-0005ghsaWEB
- security.netapp.com/advisory/ntap-20210713-0005/mitrex_refsource_CONFIRM
- spring.io/security/cve-2021-22118ghsaWEB
- tanzu.vmware.com/security/cve-2021-22118ghsax_refsource_MISCWEB
- www.oracle.com//security-alerts/cpujul2021.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpuapr2022.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpujan2022.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpujul2022.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpuoct2021.htmlghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.