spring-boot: Allow custom ErrorReportValve to be used with Tomcat and provide whitelabel version

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 https://github.com/spring-projects/spring-boot/blob/767156167b4d1d91c8af0e5bff2d1be0d5149651/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java#L295

What do you think?

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 1
  • Comments: 24 (9 by maintainers)

Most upvoted comments

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,

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.

You should use addContextCustomizers rather than setTomcatContextCustomizers as the latter will replace any existing customizers.

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

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.