添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement . We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

During a penetration test one finding was the information disclosure of using a Tomcat webserver.

If a request with an invalid URL (e.g. http://localhost:8080/[test ) is executed the configured custom error pages are not used.
Instead the embedded Tomcats ErrorReportValve is used and presents a default Tomcat Error page.

It is possible to configure it to some extends using

  • server.error.whitelabel.enabled=false
  • server.error.include-stacktrace=never
  • But the default HTTP Status 400 page is always returned.

    It is possible to create a custom ErrorReportValve and set the properties like errorCode.400 to create a custom page, but this configuration is not possible with an application.properties file.

    (At least as far as I can see)
    See an example project at https://github.com/patst/tomcat-errorvalve

    Maybe it would be a good idea to expose the properties for configuration.

    The ErrorReportValve is created at

    spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java Line 295 7671561

    We're probably not going to be able to customize the ErrorReportValve because valve.setProperty requires a file and that won't work with fat jars.

    We'd like to investigate a little more to find out why the ErrorReportValve is being used and the DispatcherServlet isn't being called at all.

    Expose Tomcat ErrorReportValve properties Tomcat invokes ErrorReportValve directly for malformed URLs May 7, 2020

    We'd like to investigate a little more to find out why the ErrorReportValve is being used and the DispatcherServlet isn't being called at all.

    This happens when the failure occurs before the request has been matched to a particular context. An illegal HTTP header or a malformed URL will both cause such a failure.

    With Jetty, a request with an illegal HTTP header produces the following response:

    HTTP/1.1 400 Illegal character VCHAR='('
    Content-Type: text/html;charset=iso-8859-1
    Content-Length: 70
    Connection: close
    <h1>Bad Message 400</h1><pre>reason: Illegal character VCHAR='('</pre>
    

    Undertow produces the following response:

    HTTP/1.1 400 Bad Request
    Content-Length: 0
    Connection: close
    

    If we can't route into the app and its error pages, Undertow's response seems like the best alternative as there's nothing in it that could be used to identify the server that is producing the response.

    The response body can be suppressed when using Jetty by configuring the Server with a custom ErrorHandler:

    @Bean
    JettyServerCustomizer serverCustomizer() {
        return (server) -> {
            server.setErrorHandler(new ErrorHandler() {
                @Override
                public ByteBuffer badMessageError(int status, String reason, HttpFields fields) {
                    return null;
    

    We can't do anything about the reason in the status line as the logic that generates it isn't pluggable.

    I hit this issue with a request header that was too large.

    Looks like I am able to get around the default configuration like this, without any changes to Spring. The key is the setErrorReportValveClass(), and the getOrder().

    @Bean
    public TomcatWebSocketServletWebServerCustomizer errorValveCustomizer() {
        return new TomcatWebSocketServletWebServerCustomizer() {
            @Override
            public void customize(TomcatServletWebServerFactory factory) {
                factory.addContextCustomizers((context) -> {
                    Container parent = context.getParent();
                    if ( parent instanceof StandardHost) {
                        ((StandardHost) parent).setErrorReportValveClass("aaa.bbb.ccc.configuration.CustomTomcatErrorValve");
            @Override
            public int getOrder() {
                return 100; // needs to be AFTER the one configured with TomcatWebServerFactoryCustomizer
    

    Then create that class aaa.bbb.ccc.configuration.CustomTomcatErrorValve

    package aaa.bbb.ccc;
    public class CustomTomcatErrorValve extends ErrorReportValve{
        private static final Logger LOGGER = LoggerFactory.getLogger(CustomTomcatErrorValve.class);
        protected void report(Request request, Response response, Throwable throwable) {
            if  (!response.setErrorReported())
                return;
            LOGGER.warn("{} Fatal error before getting to Spring. {} ", response.getStatus(), errorCode, throwable);
            try {
                Writer writer = response.getReporter();
                writer.write(Integer.toString(response.getStatus()));
                writer.write(" Fatal error.  Could not process request.");
                response.finishResponse();
            } catch (IOException e) {
    

    This valve can be configured however you'd like, BUT as discussed above, you don't have too many options because the context hasn't (may not have) been set in the Request yet, so you can't redirect and setting setProperty("errorCode.0", .... ) requires a File path. So here, I'm just returning the error text directly.

    It's also possible to do this, instead of a direct response...

    String errorCode = .....;
    LOGGER.warn("{} Fatal error before getting to Spring. {} ", response.getStatus(), errorCode, throwable);
    response.setStatus(302);
    response.setHeader("Location", "/error?errorCode="+errorCode);

    but.... I'm not sure that's better. If the tomcat request is dying so early that we can't do a normal redirect, or forward, than it's probably not really safe to expect the /error page will work either. But if you have an external error page you could reference, that could be nicer.

    I could see Spring following the above to provide an internal Spring ErrorValve that generates the disaster HTML response, and then taking it from a server.error.disaster-html type property. Or even allowing a FunctionalInterface customizer that generates text,

    Tomcat invokes ErrorReportValve directly for malformed URLs Allow custom ErrorReportValve to be used with Tomcat and provide whitelabel version Jun 1, 2021

    For anyone trying @sc-moonlight's workaround (many thanks for this!), I had to add the following line to get it to work:

    ((StandardHost) parent).addValve(new CustomTomcatErrorValve());

    @sc-moonlight's solution works fine as long as you don't apply any other customization.

    In my case I previously had this WebServerFactoryCustomizer:

    @Slf4j
    @Configuration
    @ConditionalOnClass(Tomcat.class)
    public class TomcatConfiguration implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
        @Override
        public void customize(TomcatServletWebServerFactory factory) {
            factory.setTomcatContextCustomizers(Collections.singletonList(new CustomCustomizer()));
        private static class CustomCustomizer implements TomcatContextCustomizer {
            public void customize(Context context) {
                context.setReloadable(false);
                context.setMapperContextRootRedirectEnabled(false);
    

    I added the mentioned TomcatWebSocketServletWebServerCustomizer bean and it didn't work. I tried commenting out my previous customization and then the CustomTomcatErrorValve was correctly applied:

        @Override
        public void customize(TomcatServletWebServerFactory factory) {
            //factory.setTomcatContextCustomizers(Collections.singletonList(new CustomCustomizer()));
    

    Any workaround to be able to apply both customizations?

    I have also come across this issue also raised due to pen-test findings some time back. The solution provided by @sc-moonlight and @petenattress above worked well. Combining both and making it a @Component, this is what we have so that the body is fully stripped out from the response:

    import org.apache.catalina.connector.Request;
    import org.apache.catalina.connector.Response;
    import org.apache.catalina.core.StandardHost;
    import org.apache.catalina.valves.ErrorReportValve;
    import org.springframework.boot.autoconfigure.websocket.servlet.TomcatWebSocketServletWebServerCustomizer;
    import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
    import org.springframework.stereotype.Component;
    @Component
    public class ErrorValveConfig extends TomcatWebSocketServletWebServerCustomizer {
        @Override
        public void customize(TomcatServletWebServerFactory factory) {
            factory.addContextCustomizers((context) -> {
                if (context.getParent() instanceof StandardHost parent) {
                    parent.setErrorReportValveClass(BlankBodyErrorReportValve.class.getName());
                    parent.addValve(new BlankBodyErrorReportValve());
        @Override
        public int getOrder() {
            return 100; // needs to be AFTER the one configured with TomcatWebServerFactoryCustomizer
        private static class BlankBodyErrorReportValve extends ErrorReportValve {
            @Override
            protected void report(Request request, Response response, Throwable throwable) {
                response.setErrorReported();
    

    We use that in conjunction with ExceptionHandlers for actual business logic exception handling.

    Here is my approach to this issue, based on the others above:

        @Bean
        public TomcatWebServerFactoryCustomizer blankErrorValveTomcatWebServerFactoryCustomizer(
            Environment environment,
            ServerProperties serverProperties,
            TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer
            return new TomcatWebServerFactoryCustomizer(environment, serverProperties) {
                @Override
                public void customize(ConfigurableTomcatWebServerFactory factory) {
                    factory.addContextCustomizers( context -> {
                        if(context.getParent() instanceof StandardHost standardHost) {
                            standardHost.getPipeline().addValve(new BlankTomcatErrorValve());
                            standardHost.setErrorReportValveClass(BlankTomcatErrorValve.class.getName());
                @Override
                public int getOrder() {
                    return tomcatWebServerFactoryCustomizer.getOrder() + 1;
    

    This should be pretty bulletproof. I put it in a @Configuration and used this valve to disable any error responses:

    public class BlankTomcatErrorValve extends ErrorReportValve {
        private static final Logger LOGGER = LoggerFactory.getLogger(BlankTomcatErrorValve.class);
        @Override
        protected void report(Request request, Response response, Throwable throwable) {
            LOGGER.warn("Fatal error in Servlet:", throwable);
            response.setSuspended(true);
              

    @wilkinsona @sc-moonlight @codependent @ClaudioConsolmagno @MayCXC

    Is this still working for you in Spring-Boot 2.7.5 / Tomcat 9.0.68?

    This was working for me for several versions over the past several years, including in Spring-Boot 2.7.4, but broke in 2.7.5. I've spent several hours looking at it today, but I'm not quite sure what happened.

    You may not notice it, but the default behavior in the latest version seems to be to return an HTTP 400 and absolutely no body. This isn't the behavior I want - I want it to be an HTTP 500 + and the body should be a small hardcoded string.

    I'm not sure whether this is an intended change (in which case, how are we supposed to customize this now?) or if Tomcat, Catalina, Spring-Boot, or something else has regressed.

    Or is everything still working fine for the rest of you?

    Edit: Spring-Boot 2.7.5 works for me as long as I downgrade Tomcat to 9.0.65 (the same version that came with 2.7.4). Downgrading to 9.0.67 didn't fix the issue and I wasn't able to download 9.0.66 - the release history shows that 9.0.67 was released less than a day after 9.0.66, so I assume that had a major issue and was pulled.

    Is it a bug with Tomcat 9.0.67+ or Spring-Boot 2.7.5? Or is there just a different process now that I don't know about?

    Edit 2x: I found the issue. It has nothing to do with Spring-Boot. Tomcat's ErrorReportValve was changed between 9.0.65 and 9.0.67. They moved some logic from the start of the report function to the end of the invoke function. Removing the call to "setErrorReported" from my report function appears to have fixed my issues.

    @ClaudioConsolmagno - I believe you can actually remove the one line from your report function in 2.7.5.

    The logic in the base ErrorReportValve class was rearranged. It now calls that one line just before calling report instead of just inside of it.

    It returns a Boolean (whether the function was called previously or not). I had been checking the return value of that Boolean to decide whether to write out my fixed error string or not, but since they now call it for you, it was always telling my code to do nothing. I removed that check and now my code works in 2.7.5.