VYPR
High severityNVD Advisory· Published May 27, 2021· Updated Aug 3, 2024

CVE-2021-22118

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.

PackageAffected versionsPatched versions
org.springframework:spring-webMaven
>= 5.2.0, < 5.2.155.2.15
org.springframework:spring-webMaven
>= 5.3.0, < 5.3.75.3.7

Affected products

2

Patches

2
0d0d75e25322

Ensure 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();
    +	}
    +
    +}
    
cce60c479c22

Ensure 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

News mentions

0

No linked articles in our index yet.