From 1b81d9f0b592c67d0b398cd03dc92480fd05acc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Sun, 15 Nov 2015 17:18:56 -0500 Subject: [PATCH] Add support for server.server-header property Add a `server.server-header` property which can be used to override the `server` header usually sent back automatically by Tomcat/Jetty or Undertow. See https://www.owasp.org/index.php/Securing_tomcat for background. Fixes gh-4461 Closes gh-4504 --- ...dpointWebMvcChildContextConfiguration.java | 2 + .../autoconfigure/web/ServerProperties.java | 15 +++++ .../web/ServerPropertiesTests.java | 15 +++++ .../appendix-application-properties.adoc | 1 + .../src/main/resources/application.properties | 2 +- ...tConfigurableEmbeddedServletContainer.java | 12 ++++ .../ConfigurableEmbeddedServletContainer.java | 7 +++ .../jetty/JettyEmbeddedServletContainer.java | 1 + .../JettyEmbeddedServletContainerFactory.java | 55 ++++++++++++++++--- ...TomcatEmbeddedServletContainerFactory.java | 4 ++ .../UndertowEmbeddedServletContainer.java | 14 +++++ ...dertowEmbeddedServletContainerFactory.java | 11 +++- ...tEmbeddedServletContainerFactoryTests.java | 11 ++++ 13 files changed, 138 insertions(+), 12 deletions(-) diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java index 70d9d1846bb..8059296ddf5 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java @@ -67,6 +67,7 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; * @author Dave Syer * @author Stephane Nicoll * @author Andy Wilkinson + * @author Eddú Meléndez * @see EndpointWebMvcAutoConfiguration */ @Configuration @@ -187,6 +188,7 @@ public class EndpointWebMvcChildContextConfiguration { container.setContextPath(""); // and add the management-specific bits container.setPort(this.managementServerProperties.getPort()); + container.setServerHeader(this.server.getServerHeader()); container.setAddress(this.managementServerProperties.getAddress()); container.addErrorPages(new ErrorPage(this.server.getError().getPath())); } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index df041e5dc18..e74d420df33 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -72,6 +72,7 @@ import org.springframework.util.StringUtils; * @author Andy Wilkinson * @author Ivan Sopov * @author Marcos Barbero + * @author Eddú Meléndez */ @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) public class ServerProperties @@ -116,6 +117,11 @@ public class ServerProperties */ private Boolean useForwardHeaders; + /** + * Value to use for the server header. + */ + private String serverHeader; + private Session session = new Session(); @NestedConfigurationProperty @@ -173,6 +179,7 @@ public class ServerProperties if (getCompression() != null) { container.setCompression(getCompression()); } + container.setServerHeader(getServerHeader()); if (container instanceof TomcatEmbeddedServletContainerFactory) { getTomcat().customizeTomcat(this, (TomcatEmbeddedServletContainerFactory) container); @@ -304,6 +311,14 @@ public class ServerProperties this.useForwardHeaders = useForwardHeaders; } + public String getServerHeader() { + return this.serverHeader; + } + + public void setServerHeader(String serverHeader) { + this.serverHeader = serverHeader; + } + protected final boolean getOrDeduceUseForwardHeaders() { if (this.useForwardHeaders != null) { return this.useForwardHeaders; diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 8a356f482ee..a12f8fb3a48 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -50,6 +50,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.atLeastOnce; @@ -65,6 +66,7 @@ import static org.mockito.Mockito.verify; * @author Stephane Nicoll * @author Andy Wilkinson * @author Phillip Webb + * @author Eddú Meléndez */ public class ServerPropertiesTests { @@ -94,6 +96,19 @@ public class ServerPropertiesTests { assertEquals(9000, this.properties.getPort().intValue()); } + @Test + public void testServerHeaderDefault() throws Exception { + assertNull(this.properties.getServerHeader()); + } + + @Test + public void testServerHeader() throws Exception { + RelaxedDataBinder binder = new RelaxedDataBinder(this.properties, "server"); + binder.bind(new MutablePropertyValues( + Collections.singletonMap("server.server-header", "Custom Server"))); + assertEquals("Custom Server", this.properties.getServerHeader()); + } + @Test public void testServletPathAsMapping() throws Exception { RelaxedDataBinder binder = new RelaxedDataBinder(this.properties, "server"); diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index dbdd182370c..24fb49609be 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -153,6 +153,7 @@ content into your application; rather pick only the properties that you need. server.jsp-servlet.init-parameters.*= # Init parameters used to configure the JSP servlet server.jsp-servlet.registered=true # Whether or not the JSP servlet is registered server.port=8080 # Server HTTP port. + server.server-header= # The value sent in the server response header server.servlet-path=/ # Path of the main dispatcher servlet. server.session.cookie.comment= # Comment for the session cookie. server.session.cookie.domain= # Domain for the session cookie. diff --git a/spring-boot-samples/spring-boot-sample-tomcat/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-tomcat/src/main/resources/application.properties index f0cf3217203..09ab26beccd 100644 --- a/spring-boot-samples/spring-boot-sample-tomcat/src/main/resources/application.properties +++ b/spring-boot-samples/spring-boot-sample-tomcat/src/main/resources/application.properties @@ -1,2 +1,2 @@ server.compression.enabled: true -server.compression.min-response-size: 1 \ No newline at end of file +server.compression.min-response-size: 1 diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java index 74fc2c88a00..fe36e8e012f 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java @@ -36,6 +36,7 @@ import org.springframework.util.ClassUtils; * @author Andy Wilkinson * @author Stephane Nicoll * @author Ivan Sopov + * @author Eddú Meléndez * @see AbstractEmbeddedServletContainerFactory */ public abstract class AbstractConfigurableEmbeddedServletContainer @@ -74,6 +75,8 @@ public abstract class AbstractConfigurableEmbeddedServletContainer private Compression compression; + private String serverHeader; + /** * Create a new {@link AbstractConfigurableEmbeddedServletContainer} instance. */ @@ -314,6 +317,15 @@ public abstract class AbstractConfigurableEmbeddedServletContainer this.compression = compression; } + public String getServerHeader() { + return this.serverHeader; + } + + @Override + public void setServerHeader(String serverHeader) { + this.serverHeader = serverHeader; + } + /** * Utility method that can be used by subclasses wishing to combine the specified * {@link ServletContextInitializer} parameters with those defined in this instance. diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java index 4421243c9f9..94ccb039fcd 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; * @author Dave Syer * @author Andy Wilkinson * @author Stephane Nicoll + * @author Eddú Meléndez * @see EmbeddedServletContainerFactory * @see EmbeddedServletContainerCustomizer */ @@ -189,4 +190,10 @@ public interface ConfigurableEmbeddedServletContainer { */ void setCompression(Compression compression); + /** + * Sets the server header value. + * @param serverHeader the server header value + */ + void setServerHeader(String serverHeader); + } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainer.java index 72951facc65..0e91d0b7d75 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainer.java @@ -40,6 +40,7 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Dave Syer * @author David Liu + * @author Eddú Meléndez * @see JettyEmbeddedServletContainerFactory */ public class JettyEmbeddedServletContainer implements EmbeddedServletContainer { diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java index eac6266695b..21eaef66f2a 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java @@ -27,14 +27,20 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.ForwardedRequestCustomizer; +import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -84,6 +90,7 @@ import org.springframework.util.StringUtils; * @author Dave Syer * @author Andrey Hihlovskiy * @author Andy Wilkinson + * @author Eddú Meléndez * @see #setPort(int) * @see #setConfigurations(Collection) * @see JettyEmbeddedServletContainer @@ -138,14 +145,7 @@ public class JettyEmbeddedServletContainerFactory int port = (getPort() >= 0 ? getPort() : 0); Server server = new Server(new InetSocketAddress(getAddress(), port)); configureWebAppContext(context, initializers); - if (getCompression() != null && getCompression().getEnabled()) { - HandlerWrapper gzipHandler = createGzipHandler(); - gzipHandler.setHandler(context); - server.setHandler(gzipHandler); - } - else { - server.setHandler(context); - } + server.setHandler(addHandlerWrappers(context)); this.logger.info("Server initialized with port: " + port); if (getSsl() != null && getSsl().isEnabled()) { SslContextFactory sslContextFactory = new SslContextFactory(); @@ -163,6 +163,21 @@ public class JettyEmbeddedServletContainerFactory return getJettyEmbeddedServletContainer(server); } + private Handler addHandlerWrappers(Handler handler) { + if (getCompression() != null && getCompression().getEnabled()) { + handler = applyWrapper(handler, createGzipHandler()); + } + if (StringUtils.hasText(getServerHeader())) { + handler = applyWrapper(handler, new ServerHeaderHandler(getServerHeader())); + } + return handler; + } + + private Handler applyWrapper(Handler handler, HandlerWrapper wrapper) { + wrapper.setHandler(handler); + return wrapper; + } + private HandlerWrapper createGzipHandler() { ClassLoader classLoader = getClass().getClassLoader(); if (ClassUtils.isPresent(GZIP_HANDLER_JETTY_9_2, classLoader)) { @@ -709,4 +724,28 @@ public class JettyEmbeddedServletContainerFactory } + /** + * {@link HandlerWrapper} to add a custom {@code server} header. + */ + private static class ServerHeaderHandler extends HandlerWrapper { + + private static final String SERVER_HEADER = "server"; + + private final String value; + + ServerHeaderHandler(String value) { + this.value = value; + } + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + if (!response.getHeaderNames().contains(SERVER_HEADER)) { + response.setHeader(SERVER_HEADER, this.value); + } + super.handle(target, baseRequest, request, response); + } + + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java index bc2ef19048b..fcfb4081495 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java @@ -85,6 +85,7 @@ import org.springframework.util.StringUtils; * @author Brock Mills * @author Stephane Nicoll * @author Andy Wilkinson + * @author Eddú Meléndez * @see #setPort(int) * @see #setContextLifecycleListeners(Collection) * @see TomcatEmbeddedServletContainer @@ -248,6 +249,9 @@ public class TomcatEmbeddedServletContainerFactory protected void customizeConnector(Connector connector) { int port = (getPort() >= 0 ? getPort() : 0); connector.setPort(port); + if (StringUtils.hasText(this.getServerHeader())) { + connector.setAttribute("server", this.getServerHeader()); + } if (connector.getProtocolHandler() instanceof AbstractProtocol) { customizeProtocol((AbstractProtocol) connector.getProtocolHandler()); } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java index 20f2765735c..22be949363e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainer.java @@ -57,6 +57,7 @@ import org.springframework.util.StringUtils; * * @author Ivan Sopov * @author Andy Wilkinson + * @author Eddú Meléndez * @since 1.2.0 * @see UndertowEmbeddedServletContainerFactory */ @@ -77,6 +78,8 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine private final Compression compression; + private final String serverHeader; + private Undertow undertow; private boolean started = false; @@ -89,12 +92,20 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine public UndertowEmbeddedServletContainer(Builder builder, DeploymentManager manager, String contextPath, int port, boolean useForwardHeaders, boolean autoStart, Compression compression) { + this(builder, manager, contextPath, port, useForwardHeaders, autoStart, + compression, null); + } + + public UndertowEmbeddedServletContainer(Builder builder, DeploymentManager manager, + String contextPath, int port, boolean useForwardHeaders, boolean autoStart, + Compression compression, String serverHeader) { this.builder = builder; this.manager = manager; this.contextPath = contextPath; this.useForwardHeaders = useForwardHeaders; this.autoStart = autoStart; this.compression = compression; + this.serverHeader = serverHeader; } @Override @@ -123,6 +134,9 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine if (this.useForwardHeaders) { httpHandler = Handlers.proxyPeerAddress(httpHandler); } + if (StringUtils.hasText(this.serverHeader)) { + httpHandler = Handlers.header(httpHandler, "Server", this.serverHeader); + } this.builder.setHandler(httpHandler); return this.builder.build(); } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java index 812a3a69eea..ddae4e55f9e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java @@ -89,6 +89,7 @@ import org.springframework.util.ResourceUtils; * @author Ivan Sopov * @author Andy Wilkinson * @author Marcos Barbero + * @author Eddú Meléndez * @since 1.2.0 * @see UndertowEmbeddedServletContainer */ @@ -219,8 +220,7 @@ public class UndertowEmbeddedServletContainerFactory DeploymentManager manager = createDeploymentManager(initializers); int port = getPort(); Builder builder = createBuilder(port); - return new UndertowEmbeddedServletContainer(builder, manager, getContextPath(), - port, this.useForwardHeaders, port >= 0, getCompression()); + return getUndertowEmbeddedServletContainer(builder, manager, port); } private Builder createBuilder(int port) { @@ -476,7 +476,8 @@ public class UndertowEmbeddedServletContainerFactory protected UndertowEmbeddedServletContainer getUndertowEmbeddedServletContainer( Builder builder, DeploymentManager manager, int port) { return new UndertowEmbeddedServletContainer(builder, manager, getContextPath(), - port, port >= 0, getCompression()); + port, isUseForwardHeaders(), port >= 0, getCompression(), + getServerHeader()); } @Override @@ -520,6 +521,10 @@ public class UndertowEmbeddedServletContainerFactory return this.accessLogEnabled; } + protected final boolean isUseForwardHeaders() { + return this.useForwardHeaders; + } + /** * Set if x-forward-* headers should be processed. * @param useForwardHeaders if x-forward headers should be used diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java index 6a828143584..a8106dcae2b 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java @@ -698,6 +698,17 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { assertThat(rootResource.get(), is(not(nullValue()))); } + @Test + public void customServerHeader() throws Exception { + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setServerHeader("MyServer"); + this.container = factory + .getEmbeddedServletContainer(exampleServletRegistration()); + this.container.start(); + ClientHttpResponse response = getClientResponse(getLocalUrl("/hello")); + assertThat(response.getHeaders().getFirst("server"), equalTo("MyServer")); + } + private boolean doTestCompression(int contentSize, String[] mimeTypes, String[] excludedUserAgents) throws Exception { String testContent = setUpFactoryForCompression(contentSize, mimeTypes,