spring-boot
spring-boot copied to clipboard
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?
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.
Hi, I would like to work on this request.
Thanks
@gaurav-91 Thanks for the offer to help, but would you mind not commenting on multiple issues. It sends email notifications to everyone watching the issue tracker.
If you're new to the project and looking to help, I've just flagged #21531 as a first-timers-only
issue so you might want to claim that.
This also occurs for a request with an illegal HTTP header:
$ curl -H '(request): test' http://localhost:8080/person
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 woul like to take this issue as this is my first time contributing to open source projects. Is this posible?
sorry, my mistake i thought it was a first time issue
Hey, team.
It seems I'm hitting same issue. After adding below code to only allowing expected http methods in my SecurityConfig,
public StrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
return firewall;
}
I'm always get default error page w/ below stack trace.
org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the HTTP method "LINK" was not included within the whitelist [DELETE, POST, GET, PUT]
at org.springframework.security.web.firewall.StrictHttpFirewall.rejectForbiddenHttpMethod(StrictHttpFirewall.java:316)
at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:292)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:194)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:712)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:461)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:384)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:312)
at org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:394)
at org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:253)
at org.apache.catalina.core.StandardHostValve.throwable(StandardHostValve.java:348)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:173)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:834)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:834)
But other errors/exceptions are pointing my custom error pages. Any progress for this issue?
@insukcho That looks like a different problem to me. This issue is for a situation where Tomcat rejects a request before it has been mapped to a servlet and filter chain. That's not the situation you are seeing where the request has been mapped.
If you have a question about error handling and error pages, please follow up on Stack Overflow or Gitter. If you believe you have found a bug, please open a new issue and provide a minimal sample that reproduces the behaviour you are seeing.
@wilkinsona Thank you for your feedback and good to know my issue is not related with this one.
I have already did lots of investigation in Stack Overflow and did several experiments but no luck. So, that's why I'm end up with this issue. I will try to reproduce this issue and post new issue if need. Thanks you very much.
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,
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?
You should use addContextCustomizers
rather than setTomcatContextCustomizers
as the latter will replace any existing customizers.
Thanks Andy, that fixed it.
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.
Sadly, I'm still stuck on SB2.5 for now, until I resolve a bunch of other issues. So I can't help right now
@TaylorSMarks My solution above still works with Spring-Boot 2.7.5
@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.
if spring boot is 1.3.3 how i do config ?? thanks for help
It's still appears in Spring Boot 3.2.2