I am seeing a ton of RequestRejectedException
entries in my Tomcat log (sample pasted below). These started appearing in my log file after a minor vers
I implemented a subclass of StrictHttpFirewall
that logs request information to the console and throws a new exception with a suppressed stack trace. This partially solves my problem (at least I can see the bad requests now).
If you just want to see the rejected requests without the stack trace, this is the answer you're looking for.
If you want to handle these exceptions in a controller, please refer to the accepted answer for a complete (but slightly more complex) solution.
LoggingHttpFirewall.java
This class extends StrictHttpFirewall to catch RequestRejectedException
and throws a new exception with metadata from the request and a suppressed stack trace.
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.security.web.firewall.StrictHttpFirewall;
/**
* Overrides the StrictHttpFirewall to log some useful information about blocked requests.
*/
public final class LoggingHttpFirewall extends StrictHttpFirewall
{
/**
* Logger.
*/
private static final Logger LOGGER = Logger.getLogger(LoggingHttpFirewall.class.getName());
/**
* Default constructor.
*/
public LoggingHttpFirewall()
{
super();
return;
}
/**
* Provides the request object which will be passed through the filter chain.
*
* @returns A FirewalledRequest (required by the HttpFirewall interface) which
* inconveniently breaks the general contract of ServletFilter because
* we can't upcast this to an HttpServletRequest. This prevents us
* from re-wrapping this using an HttpServletRequestWrapper.
* @throws RequestRejectedException if the request should be rejected immediately.
*/
@Override
public FirewalledRequest getFirewalledRequest(final HttpServletRequest request) throws RequestRejectedException
{
try
{
return super.getFirewalledRequest(request);
} catch (RequestRejectedException ex) {
if (LOGGER.isLoggable(Level.WARNING))
{
LOGGER.log(Level.WARNING, "Intercepted RequestBlockedException: Remote Host: " + request.getRemoteHost() + " User Agent: " + request.getHeader("User-Agent") + " Request URL: " + request.getRequestURL().toString());
}
// Wrap in a new RequestRejectedException with request metadata and a shallower stack trace.
throw new RequestRejectedException(ex.getMessage() + ".\n Remote Host: " + request.getRemoteHost() + "\n User Agent: " + request.getHeader("User-Agent") + "\n Request URL: " + request.getRequestURL().toString())
{
private static final long serialVersionUID = 1L;
@Override
public synchronized Throwable fillInStackTrace()
{
return this; // suppress the stack trace.
}
};
}
}
/**
* Provides the response which will be passed through the filter chain.
* This method isn't extensible because the request may already be committed.
* Furthermore, this is only invoked for requests that were not blocked, so we can't
* control the status or response for blocked requests here.
*
* @param response The original HttpServletResponse.
* @return the original response or a replacement/wrapper.
*/
@Override
public HttpServletResponse getFirewalledResponse(final HttpServletResponse response)
{
// Note: The FirewalledResponse class is not accessible outside the package.
return super.getFirewalledResponse(response);
}
}
WebSecurityConfig.java
In WebSecurityConfig
, set the HTTP firewall to the LoggingHttpFirewall
.
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
/**
* Default constructor.
*/
public WebSecurityConfig()
{
super();
return;
}
@Override
public final void configure(final WebSecurity web) throws Exception
{
super.configure(web);
web.httpFirewall(new LoggingHttpFirewall()); // Set the custom firewall.
return;
}
}
Results
After deploying this solution to production, I quickly discovered that the default behavior of StrictHttpFirewall
was blocking Google from indexing my site!
Aug 13, 2018 1:48:56 PM com.mycompany.spring.security.AnnotatingHttpFirewall getFirewalledRequest
WARNING: Intercepted RequestBlockedException: Remote Host: 66.249.64.223 User Agent: Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) Request URL: https://www.mycompany.com/10.1601/tx.3784;jsessionid=692804549F9AB55F45DBD0AFE2A97FFD
As soon as I discovered this, I quickly deployed a new version (included in my other answer) that looks for ;jsessionid=
and allows these requests through. There may well be other requests that should pass through as well, and now I have a way of detecting these.
For Spring security versions 5.4
and above, you could simply create a bean of the type RequestRejectedHandler
that will be injected in the Spring security filter chain
import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;
@Bean
RequestRejectedHandler requestRejectedHandler() {
// sends an error response with a configurable status code (default is 400 BAD_REQUEST)
// we can pass a different value in the constructor
return new HttpStatusRequestRejectedHandler();
}
It can be also handled by a simple filter, which will lead to 404 error response
@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class LogAndSuppressRequestRejectedExceptionFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(req, res);
} catch (RequestRejectedException e) {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
log
.warn(
"request_rejected: remote={}, user_agent={}, request_url={}",
request.getRemoteHost(),
request.getHeader(HttpHeaders.USER_AGENT),
request.getRequestURL(),
e
);
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
A quite simple way is to use web.xml; specify an error page in that file:
<error-page>
<exception-type>org.springframework.security.web.firewall.RequestRejectedException</exception-type>
<location>/request-rejected</location>
</error-page>
For the specified path (location), add a mapping in a @Controller
-annotated class:
@RequestMapping(value = "/request-rejected")
@ResponseStatus(HttpStatus.BAD_REQUEST)
public @ResponseBody String handleRequestRejected(
@RequestAttribute(RequestDispatcher.ERROR_EXCEPTION) RequestRejectedException ex,
@RequestAttribute(RequestDispatcher.ERROR_REQUEST_URI) String uri) {
String msg = ex.getMessage();
// optionally log the message and requested URI (slf4j)
logger.warn("Request with URI [{}] rejected. {}", uri, msg);
return msg;
}
Another way to handle it is by using Spring AOP. We can create an advice around the FilterChainProxy.doFilter() method that catches any RequestRejectedException(s) thrown by the HttpFirewall and translates it into a 400 BAD_REQUEST
@Aspect
@Component
public class FilterChainProxyAdvice {
@Around("execution(public void org.springframework.security.web.FilterChainProxy.doFilter(..))")
public void handleRequestRejectedException (ProceedingJoinPoint pjp) throws Throwable {
try {
pjp.proceed();
} catch (RequestRejectedException exception) {
HttpServletResponse response = (HttpServletResponse) pjp.getArgs()[1]);
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
}
I see some working solution in recent github change in this commit
It should work if you register bean of type RequestRejectedHandler
or as I see it, there is also going to be an integration via WebSecurity
in WebSecurityConfigurerAdapter
. Unfornunately, this change is not probably included in 2.3.3.RELEASE using dependency management. It should be present in Spring Security Config 5.4.0-M1. For dependency management, it is version 2.4.0-M1.
Sooner or later, people coming across this answer should see this change in standard release.