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 84df6ddb3f0..cf1f62c4df9 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. */ @@ -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; } 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 8699299ee8b..d31d9ae9b60 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 @@ -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(); 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 0bfeacf8ec6..df3d22d3975 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 @@ -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()) 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 d5b4b7c6750..fbd6d37fd03 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( @@ -586,11 +621,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();