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 Dave Syer
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Eddú Meléndez
* @see EndpointWebMvcAutoConfiguration * @see EndpointWebMvcAutoConfiguration
*/ */
@Configuration @Configuration
@ -187,6 +188,7 @@ public class EndpointWebMvcChildContextConfiguration {
container.setContextPath(""); container.setContextPath("");
// and add the management-specific bits // and add the management-specific bits
container.setPort(this.managementServerProperties.getPort()); container.setPort(this.managementServerProperties.getPort());
container.setServerHeader(this.server.getServerHeader());
container.setAddress(this.managementServerProperties.getAddress()); container.setAddress(this.managementServerProperties.getAddress());
container.addErrorPages(new ErrorPage(this.server.getError().getPath())); container.addErrorPages(new ErrorPage(this.server.getError().getPath()));
} }

View File

@ -72,6 +72,7 @@ import org.springframework.util.StringUtils;
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Ivan Sopov * @author Ivan Sopov
* @author Marcos Barbero * @author Marcos Barbero
* @author Eddú Meléndez
*/ */
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties public class ServerProperties
@ -116,6 +117,11 @@ public class ServerProperties
*/ */
private Boolean useForwardHeaders; private Boolean useForwardHeaders;
/**
* Value to use for the server header.
*/
private String serverHeader;
private Session session = new Session(); private Session session = new Session();
@NestedConfigurationProperty @NestedConfigurationProperty
@ -173,6 +179,7 @@ public class ServerProperties
if (getCompression() != null) { if (getCompression() != null) {
container.setCompression(getCompression()); container.setCompression(getCompression());
} }
container.setServerHeader(getServerHeader());
if (container instanceof TomcatEmbeddedServletContainerFactory) { if (container instanceof TomcatEmbeddedServletContainerFactory) {
getTomcat().customizeTomcat(this, getTomcat().customizeTomcat(this,
(TomcatEmbeddedServletContainerFactory) container); (TomcatEmbeddedServletContainerFactory) container);
@ -304,6 +311,14 @@ public class ServerProperties
this.useForwardHeaders = useForwardHeaders; this.useForwardHeaders = useForwardHeaders;
} }
public String getServerHeader() {
return this.serverHeader;
}
public void setServerHeader(String serverHeader) {
this.serverHeader = serverHeader;
}
protected final boolean getOrDeduceUseForwardHeaders() { protected final boolean getOrDeduceUseForwardHeaders() {
if (this.useForwardHeaders != null) { if (this.useForwardHeaders != null) {
return this.useForwardHeaders; return this.useForwardHeaders;

View File

@ -50,6 +50,7 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce;
@ -65,6 +66,7 @@ import static org.mockito.Mockito.verify;
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
* @author Eddú Meléndez
*/ */
public class ServerPropertiesTests { public class ServerPropertiesTests {
@ -94,6 +96,19 @@ public class ServerPropertiesTests {
assertEquals(9000, this.properties.getPort().intValue()); 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 @Test
public void testServletPathAsMapping() throws Exception { public void testServletPathAsMapping() throws Exception {
RelaxedDataBinder binder = new RelaxedDataBinder(this.properties, "server"); 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.init-parameters.*= # Init parameters used to configure the JSP servlet
server.jsp-servlet.registered=true # Whether or not the JSP servlet is registered server.jsp-servlet.registered=true # Whether or not the JSP servlet is registered
server.port=8080 # Server HTTP port. 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.servlet-path=/ # Path of the main dispatcher servlet.
server.session.cookie.comment= # Comment for the session cookie. server.session.cookie.comment= # Comment for the session cookie.
server.session.cookie.domain= # Domain 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.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 Andy Wilkinson
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Ivan Sopov * @author Ivan Sopov
* @author Eddú Meléndez
* @see AbstractEmbeddedServletContainerFactory * @see AbstractEmbeddedServletContainerFactory
*/ */
public abstract class AbstractConfigurableEmbeddedServletContainer public abstract class AbstractConfigurableEmbeddedServletContainer
@ -74,6 +75,8 @@ public abstract class AbstractConfigurableEmbeddedServletContainer
private Compression compression; private Compression compression;
private String serverHeader;
/** /**
* Create a new {@link AbstractConfigurableEmbeddedServletContainer} instance. * Create a new {@link AbstractConfigurableEmbeddedServletContainer} instance.
*/ */
@ -314,6 +317,15 @@ public abstract class AbstractConfigurableEmbeddedServletContainer
this.compression = compression; 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 * Utility method that can be used by subclasses wishing to combine the specified
* {@link ServletContextInitializer} parameters with those defined in this instance. * {@link ServletContextInitializer} parameters with those defined in this instance.

View File

@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit;
* @author Dave Syer * @author Dave Syer
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Eddú Meléndez
* @see EmbeddedServletContainerFactory * @see EmbeddedServletContainerFactory
* @see EmbeddedServletContainerCustomizer * @see EmbeddedServletContainerCustomizer
*/ */
@ -189,4 +190,10 @@ public interface ConfigurableEmbeddedServletContainer {
*/ */
void setCompression(Compression compression); 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 Phillip Webb
* @author Dave Syer * @author Dave Syer
* @author David Liu * @author David Liu
* @author Eddú Meléndez
* @see JettyEmbeddedServletContainerFactory * @see JettyEmbeddedServletContainerFactory
*/ */
public class JettyEmbeddedServletContainer implements EmbeddedServletContainer { public class JettyEmbeddedServletContainer implements EmbeddedServletContainer {

View File

@ -27,14 +27,20 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; 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.HttpVersion;
import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.AbstractConnector;
import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ForwardedRequestCustomizer; import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
@ -84,6 +90,7 @@ import org.springframework.util.StringUtils;
* @author Dave Syer * @author Dave Syer
* @author Andrey Hihlovskiy * @author Andrey Hihlovskiy
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Eddú Meléndez
* @see #setPort(int) * @see #setPort(int)
* @see #setConfigurations(Collection) * @see #setConfigurations(Collection)
* @see JettyEmbeddedServletContainer * @see JettyEmbeddedServletContainer
@ -138,14 +145,7 @@ public class JettyEmbeddedServletContainerFactory
int port = (getPort() >= 0 ? getPort() : 0); int port = (getPort() >= 0 ? getPort() : 0);
Server server = new Server(new InetSocketAddress(getAddress(), port)); Server server = new Server(new InetSocketAddress(getAddress(), port));
configureWebAppContext(context, initializers); configureWebAppContext(context, initializers);
if (getCompression() != null && getCompression().getEnabled()) { server.setHandler(addHandlerWrappers(context));
HandlerWrapper gzipHandler = createGzipHandler();
gzipHandler.setHandler(context);
server.setHandler(gzipHandler);
}
else {
server.setHandler(context);
}
this.logger.info("Server initialized with port: " + port); this.logger.info("Server initialized with port: " + port);
if (getSsl() != null && getSsl().isEnabled()) { if (getSsl() != null && getSsl().isEnabled()) {
SslContextFactory sslContextFactory = new SslContextFactory(); SslContextFactory sslContextFactory = new SslContextFactory();
@ -163,6 +163,21 @@ public class JettyEmbeddedServletContainerFactory
return getJettyEmbeddedServletContainer(server); 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() { private HandlerWrapper createGzipHandler() {
ClassLoader classLoader = getClass().getClassLoader(); ClassLoader classLoader = getClass().getClassLoader();
if (ClassUtils.isPresent(GZIP_HANDLER_JETTY_9_2, classLoader)) { 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 Brock Mills
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Eddú Meléndez
* @see #setPort(int) * @see #setPort(int)
* @see #setContextLifecycleListeners(Collection) * @see #setContextLifecycleListeners(Collection)
* @see TomcatEmbeddedServletContainer * @see TomcatEmbeddedServletContainer
@ -248,6 +249,9 @@ public class TomcatEmbeddedServletContainerFactory
protected void customizeConnector(Connector connector) { protected void customizeConnector(Connector connector) {
int port = (getPort() >= 0 ? getPort() : 0); int port = (getPort() >= 0 ? getPort() : 0);
connector.setPort(port); connector.setPort(port);
if (StringUtils.hasText(this.getServerHeader())) {
connector.setAttribute("server", this.getServerHeader());
}
if (connector.getProtocolHandler() instanceof AbstractProtocol) { if (connector.getProtocolHandler() instanceof AbstractProtocol) {
customizeProtocol((AbstractProtocol<?>) connector.getProtocolHandler()); customizeProtocol((AbstractProtocol<?>) connector.getProtocolHandler());
} }

View File

@ -57,6 +57,7 @@ import org.springframework.util.StringUtils;
* *
* @author Ivan Sopov * @author Ivan Sopov
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Eddú Meléndez
* @since 1.2.0 * @since 1.2.0
* @see UndertowEmbeddedServletContainerFactory * @see UndertowEmbeddedServletContainerFactory
*/ */
@ -77,6 +78,8 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine
private final Compression compression; private final Compression compression;
private final String serverHeader;
private Undertow undertow; private Undertow undertow;
private boolean started = false; private boolean started = false;
@ -89,12 +92,20 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine
public UndertowEmbeddedServletContainer(Builder builder, DeploymentManager manager, public UndertowEmbeddedServletContainer(Builder builder, DeploymentManager manager,
String contextPath, int port, boolean useForwardHeaders, boolean autoStart, String contextPath, int port, boolean useForwardHeaders, boolean autoStart,
Compression compression) { 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.builder = builder;
this.manager = manager; this.manager = manager;
this.contextPath = contextPath; this.contextPath = contextPath;
this.useForwardHeaders = useForwardHeaders; this.useForwardHeaders = useForwardHeaders;
this.autoStart = autoStart; this.autoStart = autoStart;
this.compression = compression; this.compression = compression;
this.serverHeader = serverHeader;
} }
@Override @Override
@ -123,6 +134,9 @@ public class UndertowEmbeddedServletContainer implements EmbeddedServletContaine
if (this.useForwardHeaders) { if (this.useForwardHeaders) {
httpHandler = Handlers.proxyPeerAddress(httpHandler); httpHandler = Handlers.proxyPeerAddress(httpHandler);
} }
if (StringUtils.hasText(this.serverHeader)) {
httpHandler = Handlers.header(httpHandler, "Server", this.serverHeader);
}
this.builder.setHandler(httpHandler); this.builder.setHandler(httpHandler);
return this.builder.build(); return this.builder.build();
} }

View File

@ -89,6 +89,7 @@ import org.springframework.util.ResourceUtils;
* @author Ivan Sopov * @author Ivan Sopov
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Marcos Barbero * @author Marcos Barbero
* @author Eddú Meléndez
* @since 1.2.0 * @since 1.2.0
* @see UndertowEmbeddedServletContainer * @see UndertowEmbeddedServletContainer
*/ */
@ -219,8 +220,7 @@ public class UndertowEmbeddedServletContainerFactory
DeploymentManager manager = createDeploymentManager(initializers); DeploymentManager manager = createDeploymentManager(initializers);
int port = getPort(); int port = getPort();
Builder builder = createBuilder(port); Builder builder = createBuilder(port);
return new UndertowEmbeddedServletContainer(builder, manager, getContextPath(), return getUndertowEmbeddedServletContainer(builder, manager, port);
port, this.useForwardHeaders, port >= 0, getCompression());
} }
private Builder createBuilder(int port) { private Builder createBuilder(int port) {
@ -476,7 +476,8 @@ public class UndertowEmbeddedServletContainerFactory
protected UndertowEmbeddedServletContainer getUndertowEmbeddedServletContainer( protected UndertowEmbeddedServletContainer getUndertowEmbeddedServletContainer(
Builder builder, DeploymentManager manager, int port) { Builder builder, DeploymentManager manager, int port) {
return new UndertowEmbeddedServletContainer(builder, manager, getContextPath(), return new UndertowEmbeddedServletContainer(builder, manager, getContextPath(),
port, port >= 0, getCompression()); port, isUseForwardHeaders(), port >= 0, getCompression(),
getServerHeader());
} }
@Override @Override
@ -520,6 +521,10 @@ public class UndertowEmbeddedServletContainerFactory
return this.accessLogEnabled; return this.accessLogEnabled;
} }
protected final boolean isUseForwardHeaders() {
return this.useForwardHeaders;
}
/** /**
* Set if x-forward-* headers should be processed. * Set if x-forward-* headers should be processed.
* @param useForwardHeaders if x-forward headers should be used * @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()))); 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, private boolean doTestCompression(int contentSize, String[] mimeTypes,
String[] excludedUserAgents) throws Exception { String[] excludedUserAgents) throws Exception {
String testContent = setUpFactoryForCompression(contentSize, mimeTypes, String testContent = setUpFactoryForCompression(contentSize, mimeTypes,