Add properties for new max part count and max part header size
Build and Deploy Snapshot / Build and Deploy Snapshot (push) Waiting to run Details
Build and Deploy Snapshot / Trigger Docs Build (push) Blocked by required conditions Details
Build and Deploy Snapshot / Verify (push) Blocked by required conditions Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:false version:17], map[id:${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name:Linux]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:false version:17], map[id:windows-latest name:Windows]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:21], map[id:${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name:Linux]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:21], map[id:windows-latest name:Windows]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:23], map[id:${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name:Linux]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:23], map[id:windows-latest name:Windows]) (push) Waiting to run Details
Run System Tests / Java ${{ matrix.java.version}} (map[toolchain:false version:17]) (push) Waiting to run Details
Run System Tests / Java ${{ matrix.java.version}} (map[toolchain:true version:21]) (push) Waiting to run Details

To address CVE-2025-48976 and CVE-2025-48988, Tomcat 10.1.42 has
introduced two new configuration settings – maxPartCount and
maxPartHeaderSize. The default values for these configuration
settings have proven hard to get right and some applications have
had to increase the default limits. To ease their configuration in
Spring Boot, this commit introduces configuration properties for
the new settings:

-  server.tomcat.max-part-count (maxPartCount)
-  server.tomcat.max-part-header-size (maxPartHeaderSize)

The defaults are aligned with those of Tomcat 10.1.42
(10 and 512 bytes respectively).

Closes gh-45869
This commit is contained in:
Andy Wilkinson 2025-06-18 08:54:17 +01:00
parent 0f77dcb402
commit d9e4b66eee
4 changed files with 123 additions and 3 deletions

View File

@ -412,6 +412,20 @@ public class ServerProperties {
*/
private DataSize maxHttpFormPostSize = DataSize.ofMegabytes(2);
/**
* Maximum per-part header size permitted in a multipart/form-data request.
* Requests that exceed this limit will be rejected. A value of less than 0 means
* no limit.
*/
private DataSize maxPartHeaderSize = DataSize.ofBytes(512);
/**
* Maximum total number of parts permitted in a multipart/form-data request.
* Requests that exceed this limit will be rejected. A value of less than 0 means
* no limit.
*/
private int maxPartCount = 10;
/**
* Maximum amount of request body to swallow.
*/
@ -528,6 +542,22 @@ public class ServerProperties {
this.maxHttpFormPostSize = maxHttpFormPostSize;
}
public DataSize getMaxPartHeaderSize() {
return this.maxPartHeaderSize;
}
public void setMaxPartHeaderSize(DataSize maxPartHeaderSize) {
this.maxPartHeaderSize = maxPartHeaderSize;
}
public int getMaxPartCount() {
return this.maxPartCount;
}
public void setMaxPartCount(int maxPartCount) {
this.maxPartCount = maxPartCount;
}
public Accesslog getAccesslog() {
return this.accesslog;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -119,6 +119,10 @@ public class TomcatWebServerFactoryCustomizer
.asInt(DataSize::toBytes)
.when((maxHttpFormPostSize) -> maxHttpFormPostSize != 0)
.to((maxHttpFormPostSize) -> customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize));
map.from(properties::getMaxPartHeaderSize)
.asInt(DataSize::toBytes)
.to((maxPartHeaderSize) -> customizeMaxPartHeaderSize(factory, maxPartHeaderSize));
map.from(properties::getMaxPartCount).to((maxPartCount) -> customizeMaxPartCount(factory, maxPartCount));
map.from(properties::getAccesslog)
.when(ServerProperties.Tomcat.Accesslog::isEnabled)
.to((enabled) -> customizeAccessLog(factory));
@ -304,6 +308,28 @@ public class TomcatWebServerFactoryCustomizer
factory.addConnectorCustomizers((connector) -> connector.setMaxPostSize(maxHttpFormPostSize));
}
private void customizeMaxPartCount(ConfigurableTomcatWebServerFactory factory, int maxPartCount) {
factory.addConnectorCustomizers((connector) -> {
try {
connector.setMaxPartCount(maxPartCount);
}
catch (NoSuchMethodError ex) {
// Tomcat < 10.1.42
}
});
}
private void customizeMaxPartHeaderSize(ConfigurableTomcatWebServerFactory factory, int maxPartHeaderSize) {
factory.addConnectorCustomizers((connector) -> {
try {
connector.setMaxPartHeaderSize(maxPartHeaderSize);
}
catch (NoSuchMethodError ex) {
// Tomcat < 10.1.42
}
});
}
private void customizeAccessLog(ConfigurableTomcatWebServerFactory factory) {
ServerProperties.Tomcat tomcatProperties = this.serverProperties.getTomcat();
AccessLogValve valve = new AccessLogValve();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -253,6 +253,18 @@ class ServerPropertiesTests {
assertThat(this.properties.getTomcat().getThreads().getMinSpare()).isEqualTo(10);
}
@Test
void customizeTomcatMaxPartCount() {
bind("server.tomcat.max-part-count", "5");
assertThat(this.properties.getTomcat().getMaxPartCount()).isEqualTo(5);
}
@Test
void customizeTomcatMaxPartHeaderSize() {
bind("server.tomcat.max-part-header-size", "128");
assertThat(this.properties.getTomcat().getMaxPartHeaderSize()).isEqualTo(DataSize.ofBytes(128));
}
@Test
void testCustomizeJettyAcceptors() {
bind("server.jetty.threads.acceptors", "10");
@ -392,6 +404,17 @@ class ServerPropertiesTests {
.isEqualTo(getDefaultConnector().getMaxPostSize());
}
@Test
void tomcatMaxPartCountMatchesConnectorDefault() {
assertThat(this.properties.getTomcat().getMaxPartCount()).isEqualTo(getDefaultConnector().getMaxPartCount());
}
@Test
void tomcatMaxPartHeaderSizeMatchesConnectorDefault() {
assertThat(this.properties.getTomcat().getMaxPartHeaderSize().toBytes())
.isEqualTo(getDefaultConnector().getMaxPartHeaderSize());
}
@Test
void tomcatUriEncodingMatchesConnectorDefault() {
assertThat(this.properties.getTomcat().getUriEncoding().name())

View File

@ -37,6 +37,8 @@ import org.springframework.boot.autoconfigure.web.ServerProperties.ForwardHeader
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.testsupport.classpath.ClassPathOverrides;
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.server.WebServer;
@ -45,6 +47,7 @@ import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for {@link TomcatWebServerFactoryCustomizer}
@ -60,6 +63,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Parviz Rozikov
* @author Moritz Halbritter
*/
@DirtiesUrlFactories
class TomcatWebServerFactoryCustomizerTests {
private MockEnvironment environment;
@ -177,6 +181,37 @@ class TomcatWebServerFactoryCustomizerTests {
(server) -> assertThat(server.getTomcat().getConnector().getMaxPostSize()).isEqualTo(10000));
}
@Test
void defaultMaxPartCount() {
customizeAndRunServer(
(server) -> assertThat(server.getTomcat().getConnector().getMaxPartCount()).isEqualTo(10));
}
@Test
void customMaxPartCount() {
bind("server.tomcat.max-part-count=5");
customizeAndRunServer((server) -> assertThat(server.getTomcat().getConnector().getMaxPartCount()).isEqualTo(5));
}
@Test
void defaultMaxPartHeaderSize() {
customizeAndRunServer(
(server) -> assertThat(server.getTomcat().getConnector().getMaxPartHeaderSize()).isEqualTo(512));
}
@Test
void customMaxPartHeaderSize() {
bind("server.tomcat.max-part-header-size=4KB");
customizeAndRunServer(
(server) -> assertThat(server.getTomcat().getConnector().getMaxPartHeaderSize()).isEqualTo(4096));
}
@Test
@ClassPathOverrides("org.apache.tomcat.embed:tomcat-embed-core:10.1.41")
void customizerIsCompatibleWithTomcatVersionsWithoutMaxPartCountAndMaxPartHeaderSize() {
assertThatNoException().isThrownBy(this::customizeAndRunServer);
}
@Test
void defaultMaxHttpRequestHeaderSize() {
customizeAndRunServer((server) -> assertThat(
@ -586,11 +621,17 @@ class TomcatWebServerFactoryCustomizerTests {
Bindable.ofInstance(this.serverProperties));
}
private void customizeAndRunServer() {
customizeAndRunServer(null);
}
private void customizeAndRunServer(Consumer<TomcatWebServer> consumer) {
TomcatWebServer server = customizeAndGetServer();
server.start();
try {
consumer.accept(server);
if (consumer != null) {
consumer.accept(server);
}
}
finally {
server.stop();