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
This commit is contained in:
Eddú Meléndez 2015-11-15 17:18:56 -05:00 committed by Phillip Webb
parent b2f1355e74
commit 1b81d9f0b5
13 changed files with 138 additions and 12 deletions

View File

@ -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()));
}

View File

@ -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;

View File

@ -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");

View File

@ -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.

View File

@ -1,2 +1,2 @@
server.compression.enabled: true
server.compression.min-response-size: 1
server.compression.min-response-size: 1

View File

@ -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.

View File

@ -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);
}

View File

@ -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 {

View File

@ -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);
}
}
}

View File

@ -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());
}

View File

@ -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();
}

View File

@ -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

View File

@ -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,