Skip to content

Commit d9e4b66

Browse files
committed
Add properties for new max part count and max part header size
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
1 parent 0f77dcb commit d9e4b66

File tree

4 files changed

+123
-3
lines changed

4 files changed

+123
-3
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,20 @@ public static class Tomcat {
412412
*/
413413
private DataSize maxHttpFormPostSize = DataSize.ofMegabytes(2);
414414

415+
/**
416+
* Maximum per-part header size permitted in a multipart/form-data request.
417+
* Requests that exceed this limit will be rejected. A value of less than 0 means
418+
* no limit.
419+
*/
420+
private DataSize maxPartHeaderSize = DataSize.ofBytes(512);
421+
422+
/**
423+
* Maximum total number of parts permitted in a multipart/form-data request.
424+
* Requests that exceed this limit will be rejected. A value of less than 0 means
425+
* no limit.
426+
*/
427+
private int maxPartCount = 10;
428+
415429
/**
416430
* Maximum amount of request body to swallow.
417431
*/
@@ -528,6 +542,22 @@ public void setMaxHttpFormPostSize(DataSize maxHttpFormPostSize) {
528542
this.maxHttpFormPostSize = maxHttpFormPostSize;
529543
}
530544

545+
public DataSize getMaxPartHeaderSize() {
546+
return this.maxPartHeaderSize;
547+
}
548+
549+
public void setMaxPartHeaderSize(DataSize maxPartHeaderSize) {
550+
this.maxPartHeaderSize = maxPartHeaderSize;
551+
}
552+
553+
public int getMaxPartCount() {
554+
return this.maxPartCount;
555+
}
556+
557+
public void setMaxPartCount(int maxPartCount) {
558+
this.maxPartCount = maxPartCount;
559+
}
560+
531561
public Accesslog getAccesslog() {
532562
return this.accesslog;
533563
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -119,6 +119,10 @@ public void customize(ConfigurableTomcatWebServerFactory factory) {
119119
.asInt(DataSize::toBytes)
120120
.when((maxHttpFormPostSize) -> maxHttpFormPostSize != 0)
121121
.to((maxHttpFormPostSize) -> customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize));
122+
map.from(properties::getMaxPartHeaderSize)
123+
.asInt(DataSize::toBytes)
124+
.to((maxPartHeaderSize) -> customizeMaxPartHeaderSize(factory, maxPartHeaderSize));
125+
map.from(properties::getMaxPartCount).to((maxPartCount) -> customizeMaxPartCount(factory, maxPartCount));
122126
map.from(properties::getAccesslog)
123127
.when(ServerProperties.Tomcat.Accesslog::isEnabled)
124128
.to((enabled) -> customizeAccessLog(factory));
@@ -304,6 +308,28 @@ private void customizeMaxHttpFormPostSize(ConfigurableTomcatWebServerFactory fac
304308
factory.addConnectorCustomizers((connector) -> connector.setMaxPostSize(maxHttpFormPostSize));
305309
}
306310

311+
private void customizeMaxPartCount(ConfigurableTomcatWebServerFactory factory, int maxPartCount) {
312+
factory.addConnectorCustomizers((connector) -> {
313+
try {
314+
connector.setMaxPartCount(maxPartCount);
315+
}
316+
catch (NoSuchMethodError ex) {
317+
// Tomcat < 10.1.42
318+
}
319+
});
320+
}
321+
322+
private void customizeMaxPartHeaderSize(ConfigurableTomcatWebServerFactory factory, int maxPartHeaderSize) {
323+
factory.addConnectorCustomizers((connector) -> {
324+
try {
325+
connector.setMaxPartHeaderSize(maxPartHeaderSize);
326+
}
327+
catch (NoSuchMethodError ex) {
328+
// Tomcat < 10.1.42
329+
}
330+
});
331+
}
332+
307333
private void customizeAccessLog(ConfigurableTomcatWebServerFactory factory) {
308334
ServerProperties.Tomcat tomcatProperties = this.serverProperties.getTomcat();
309335
AccessLogValve valve = new AccessLogValve();

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -253,6 +253,18 @@ void testCustomizeTomcatMinSpareThreads() {
253253
assertThat(this.properties.getTomcat().getThreads().getMinSpare()).isEqualTo(10);
254254
}
255255

256+
@Test
257+
void customizeTomcatMaxPartCount() {
258+
bind("server.tomcat.max-part-count", "5");
259+
assertThat(this.properties.getTomcat().getMaxPartCount()).isEqualTo(5);
260+
}
261+
262+
@Test
263+
void customizeTomcatMaxPartHeaderSize() {
264+
bind("server.tomcat.max-part-header-size", "128");
265+
assertThat(this.properties.getTomcat().getMaxPartHeaderSize()).isEqualTo(DataSize.ofBytes(128));
266+
}
267+
256268
@Test
257269
void testCustomizeJettyAcceptors() {
258270
bind("server.jetty.threads.acceptors", "10");
@@ -392,6 +404,17 @@ void tomcatMaxHttpFormPostSizeMatchesConnectorDefault() {
392404
.isEqualTo(getDefaultConnector().getMaxPostSize());
393405
}
394406

407+
@Test
408+
void tomcatMaxPartCountMatchesConnectorDefault() {
409+
assertThat(this.properties.getTomcat().getMaxPartCount()).isEqualTo(getDefaultConnector().getMaxPartCount());
410+
}
411+
412+
@Test
413+
void tomcatMaxPartHeaderSizeMatchesConnectorDefault() {
414+
assertThat(this.properties.getTomcat().getMaxPartHeaderSize().toBytes())
415+
.isEqualTo(getDefaultConnector().getMaxPartHeaderSize());
416+
}
417+
395418
@Test
396419
void tomcatUriEncodingMatchesConnectorDefault() {
397420
assertThat(this.properties.getTomcat().getUriEncoding().name())

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import org.springframework.boot.context.properties.bind.Bindable;
3838
import org.springframework.boot.context.properties.bind.Binder;
3939
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
40+
import org.springframework.boot.testsupport.classpath.ClassPathOverrides;
41+
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
4042
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
4143
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
4244
import org.springframework.boot.web.server.WebServer;
@@ -45,6 +47,7 @@
4547
import org.springframework.util.unit.DataSize;
4648

4749
import static org.assertj.core.api.Assertions.assertThat;
50+
import static org.assertj.core.api.Assertions.assertThatNoException;
4851

4952
/**
5053
* Tests for {@link TomcatWebServerFactoryCustomizer}
@@ -60,6 +63,7 @@
6063
* @author Parviz Rozikov
6164
* @author Moritz Halbritter
6265
*/
66+
@DirtiesUrlFactories
6367
class TomcatWebServerFactoryCustomizerTests {
6468

6569
private MockEnvironment environment;
@@ -177,6 +181,37 @@ void customMaxHttpFormPostSize() {
177181
(server) -> assertThat(server.getTomcat().getConnector().getMaxPostSize()).isEqualTo(10000));
178182
}
179183

184+
@Test
185+
void defaultMaxPartCount() {
186+
customizeAndRunServer(
187+
(server) -> assertThat(server.getTomcat().getConnector().getMaxPartCount()).isEqualTo(10));
188+
}
189+
190+
@Test
191+
void customMaxPartCount() {
192+
bind("server.tomcat.max-part-count=5");
193+
customizeAndRunServer((server) -> assertThat(server.getTomcat().getConnector().getMaxPartCount()).isEqualTo(5));
194+
}
195+
196+
@Test
197+
void defaultMaxPartHeaderSize() {
198+
customizeAndRunServer(
199+
(server) -> assertThat(server.getTomcat().getConnector().getMaxPartHeaderSize()).isEqualTo(512));
200+
}
201+
202+
@Test
203+
void customMaxPartHeaderSize() {
204+
bind("server.tomcat.max-part-header-size=4KB");
205+
customizeAndRunServer(
206+
(server) -> assertThat(server.getTomcat().getConnector().getMaxPartHeaderSize()).isEqualTo(4096));
207+
}
208+
209+
@Test
210+
@ClassPathOverrides("org.apache.tomcat.embed:tomcat-embed-core:10.1.41")
211+
void customizerIsCompatibleWithTomcatVersionsWithoutMaxPartCountAndMaxPartHeaderSize() {
212+
assertThatNoException().isThrownBy(this::customizeAndRunServer);
213+
}
214+
180215
@Test
181216
void defaultMaxHttpRequestHeaderSize() {
182217
customizeAndRunServer((server) -> assertThat(
@@ -586,11 +621,17 @@ private void bind(String... inlinedProperties) {
586621
Bindable.ofInstance(this.serverProperties));
587622
}
588623

624+
private void customizeAndRunServer() {
625+
customizeAndRunServer(null);
626+
}
627+
589628
private void customizeAndRunServer(Consumer<TomcatWebServer> consumer) {
590629
TomcatWebServer server = customizeAndGetServer();
591630
server.start();
592631
try {
593-
consumer.accept(server);
632+
if (consumer != null) {
633+
consumer.accept(server);
634+
}
594635
}
595636
finally {
596637
server.stop();

0 commit comments

Comments
 (0)