diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index ea39dc68f6a..a895a9f4ef5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -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. */ @@ -524,6 +538,22 @@ public class ServerProperties { */ private UseApr useApr = UseApr.NEVER; + 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; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java index c60ce2cffe0..f8aa0d330f4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java @@ -121,6 +121,10 @@ public class TomcatWebServerFactoryCustomizer .to((maxHttpFormPostSize) -> customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize)); map.from(properties::getMaxParameterCount) .to((maxParameterCount) -> customizeMaxParameterCount(factory, maxParameterCount)); + 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)); @@ -298,6 +302,28 @@ public class TomcatWebServerFactoryCustomizer factory.addConnectorCustomizers((connector) -> connector.setMaxParameterCount(maxParameterCount)); } + 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(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 55c41832809..130ffdedd3a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -260,6 +260,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"); @@ -405,6 +417,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()) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java index cc34e5359ce..5e69cb602a4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java @@ -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( @@ -593,11 +628,17 @@ class TomcatWebServerFactoryCustomizerTests { Bindable.ofInstance(this.serverProperties)); } + private void customizeAndRunServer() { + customizeAndRunServer(null); + } + private void customizeAndRunServer(Consumer consumer) { TomcatWebServer server = customizeAndGetServer(); server.start(); try { - consumer.accept(server); + if (consumer != null) { + consumer.accept(server); + } } finally { server.stop();