How to intercept a RequestRejectedException in Spring?
Asked Answered
S

10

53

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 version upgrade (Spring Security 4.2.4, IIRC) a few months ago, so this is clearly a new security feature in Spring that is enabled by default. A similar issue is reported here, but my question involves specifically how to intercept these exceptions in a controller. There is a Spring Security bug documented for this problem (Provide a way to handle RequestRejectedException). However, they aren't targeting a fix for this problem until Spring 5.1.

I understand why these exceptions are being thrown, and I do not want to disable this security feature.

I want to gain some control over this feature so that:

  1. I know I'm not blocking legitimate users from my site.
  2. I can see what requests are triggering this (are they SQL Injection attacks?)
  3. I can adjust the server response. The Spring Security firewall dumps a complete stack trace to the web client (information disclosure), along with a 500 Internal Server Error (which is wildly incorrect, this should be a 400 Bad Request).

I want to find a way to log the URL that was requested, but also suppress the stack trace specifically for these exceptions because they are polluting my log files without giving me any helpful information. Optimally, I'd like to intercept these exceptions and handle them in my application layer instead of reporting them in the Tomcat log at all.

For example, this is one of thousands of these log entries that appear every day in my catalina.out:

Aug 10, 2018 2:01:36 PM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [dispatcher] in context with path [] threw exception
org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
        at org.springframework.security.web.firewall.StrictHttpFirewall.rejectedBlacklistedUrls(StrictHttpFirewall.java:265)
        at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:245)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:193)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177)
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:347)
        at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:263)
        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.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:496)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
        at org.apache.coyote.ajp.AjpProcessor.service(AjpProcessor.java:486)
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459)
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

I'm seeing over 3,200 of these in a two day period, and it has quickly become the single largest contributor to my catalina.out log file, to the point that it prevents me from seeing other, legitimate problems. Essentially, this new Spring Security feature is a form of built-in Denial-of-Service, and it has wasted hours of my time since April. I am not saying that it is not an important feature, simply that the default implementation is completely botched, and I want to find a way gain some control over it, both as a developer and as a systems administrator.

I use a custom Error Controller for intercepting many other Exception types (including IOException) in Spring. However, RequestRejectedException seems to be falling through for some reason.

This is the relevant part of my ErrorController.java, to give an idea of what I'm trying to accomplish:

@ControllerAdvice
public final class ErrorController
{
    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(ErrorController.class.getName());

    /**
     * Generates an Error page by intercepting exceptions generated from HttpFirewall.
     *
     * @param ex A RequestRejectedException exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(RequestRejectedException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleRequestRejectedException(final HttpServletRequest request, final RequestRejectedException ex)
    {
        if (LOGGER.isLoggable(Level.INFO))
        {
            LOGGER.log(Level.INFO, "Request Rejected", ex);
        }

        LOGGER.log(Level.WARNING, "Rejected request for [" + request.getRequestURL().toString() + "]. Reason: " + ex.getMessage());
        return "errorPage";
    }

    /**
     * Generates a Server Error page.
     *
     * @param ex An exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String handleException(final Exception ex)
    {
        if (LOGGER.isLoggable(Level.SEVERE))
        {
            LOGGER.log(Level.SEVERE, "Server Error", ex);
        }

        return "errorPage";
    }
}

This error controller works for many exceptions. For example, it successfully intercepted this IllegalStateException:

Aug 05, 2018 7:50:30 AM com.mycompany.spring.controller.ErrorController handleException
SEVERE: Server Error
java.lang.IllegalStateException: Cannot create a session after the response has been committed
        at org.apache.catalina.connector.Request.doGetSession(Request.java:2999)
...

However, this is not intercepting RequestRejectedException (as indicated by the lack of "Server Error" in the first log sample above).

How can I intercept RequestRejectedException in an error controller?

Seam answered 10/8, 2018 at 14:33 Comment(3)
What I can't figure out is WHO is adding the extra slash? I type "example.com/app/main" into the browser's address field, and Spring is somehow getting "/app//main".Targe
Recorded as Spring Security bug 5007. Noted as fixed in version 5.4.0-M1 in July 2020; version 5.4.0 was released in September 2020.Rebatement
However, I am having the same problem with Spring Security 5.4.2. So I guess they have not so much fixed the problem, as made it easier to implement a work-around.Rebatement
S
14

It turns out that although HttpFirewall and StrictHttpFirewall contain several design errors (documented in the code below), it is just barely possible to escape Spring Security's One True Firewall and tunnel the HttpFirewall information via a request attribute to a HandlerInterceptor that can pass these flagged requests to a real (persistent) firewall without sacrificing the original business logic that flagged them in the first place. The method documented here should be fairly future-proof, as it conforms to a simple contract from the HttpFirewall interface, and the rest is simply the core Spring Framework and Java Servlet API.

This is essentially a more complicated but more complete alternative to my earlier answer. In this answer, I implemented a new subclass of StrictHttpFirewall that intercepts and logs rejected requests at a specific logging level, but also adds an attribute to the HTTP request that flags it for downstream filters (or controllers) to handle. Also, this AnnotatingHttpFirewall provides an inspect() method that allows subclasses to add custom rules for blocking requests.

This solution is split into two parts: (1) Spring Security and (2) Spring Framework (Core), because that is the divide that caused this problem in the first place, and this shows how to bridge it.

For reference, this is tested on Spring 4.3.17 and Spring Security 4.2.6. There may be significant changes when Spring 5.1 is released.


Part 1: Spring Security

This is the half of the solution that performs the logging and flagging within Spring Security.


AnnotatingHttpFirewall.java

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 class AnnotatingHttpFirewall extends StrictHttpFirewall
{
    /**
     * The name of the HTTP header representing a request that has been rejected by this firewall.
     */
    public static final String HTTP_HEADER_REQUEST_REJECTED_FLAG = "X-HttpFirewall-RequestRejectedFlag";

    /**
     * The name of the HTTP header representing the reason a request has been rejected by this firewall.
     */
    public static final String HTTP_HEADER_REQUEST_REJECTED_REASON = "X-HttpFirewall-RequestRejectedReason";

    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(AnnotatingHttpFirewall.class.getName());

    /**
     * Default constructor.
     */
    public AnnotatingHttpFirewall()
    {
        super();
        return;
    }

    /**
     * Provides the request object which will be passed through the filter chain.
     *
     * @param request The original HttpServletRequest.
     * @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.
     */
    @Override
    public FirewalledRequest getFirewalledRequest(final HttpServletRequest request)
    {
        try
        {
            this.inspect(request); // Perform any additional checks that the naive "StrictHttpFirewall" misses.
            return super.getFirewalledRequest(request);
        } catch (RequestRejectedException ex) {
            final String requestUrl = request.getRequestURL().toString();

            // Override some of the default behavior because some requests are
            // legitimate.
            if (requestUrl.contains(";jsessionid="))
            {
                // Do not block non-cookie serialized sessions. Google's crawler does this often.
            } else {
                // Log anything that is blocked so we can find these in the catalina.out log.
                // This will give us any information we need to make
                // adjustments to these special cases and see potentially
                // malicious activity.
                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());
                }

                // Mark this request as rejected.
                request.setAttribute(HTTP_HEADER_REQUEST_REJECTED, Boolean.TRUE);
                request.setAttribute(HTTP_HEADER_REQUEST_REJECTED_REASON, ex.getMessage());
            }

            // Suppress the RequestBlockedException and pass the request through
            // with the additional attribute.
            return new FirewalledRequest(request)
            {
                @Override
                public void reset()
                {
                    return;
                }
            };
        }
    }

    /**
     * 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);
    }

    /**
     * Perform any custom checks on the request.
     * This method may be overridden by a subclass in order to supplement or replace these tests.
     *
     * @param request The original HttpServletRequest.
     * @throws RequestRejectedException if the request should be rejected immediately.
     */
    public void inspect(final HttpServletRequest request) throws RequestRejectedException
    {
        final String requestUri = request.getRequestURI(); // path without parameters
//        final String requestUrl = request.getRequestURL().toString(); // full path with parameters

        if (requestUri.endsWith("/wp-login.php"))
        {
            throw new RequestRejectedException("The request was rejected because it is a vulnerability scan.");
        }

        if (requestUri.endsWith(".php"))
        {
            throw new RequestRejectedException("The request was rejected because it is a likely vulnerability scan.");
        }

        return; // The request passed all custom tests.
    }
}

WebSecurityConfig.java

In WebSecurityConfig, set the HTTP firewall to the AnnotatingHttpFirewall.

@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 AnnotatingHttpFirewall()); // Set the custom firewall.
        return;
    }
}

Part 2: Spring Framework

The second part of this solution could conceivably be implemented as a ServletFilter or HandlerInterceptor. I'm going the path of a HandlerInterceptor because it seems to give the most flexibility and works directly within the Spring Framework.


RequestBlockedException.java

This custom exception can be handled by an Error Controller. This may be extended to add any request headers, parameters or properties available from the raw request (even the full request itself) that may be pertinent to application business logic (e.g., a persistent firewall).

/**
 * A custom exception for situations where a request is blocked or rejected.
 */
public class RequestBlockedException extends RuntimeException
{
    private static final long serialVersionUID = 1L;

    /**
     * The requested URL.
     */
    private String requestUrl;

    /**
     * The remote address of the client making the request.
     */
    private String remoteAddress;

    /**
     * A message or reason for blocking the request.
     */
    private String reason;

    /**
     * The user agent supplied by the client the request.
     */
    private String userAgent;

    /**
     * Creates a new Request Blocked Exception.
     *
     * @param reqUrl The requested URL.
     * @param remoteAddr The remote address of the client making the request.
     * @param userAgent The user agent supplied by the client making the request.
     * @param message A message or reason for blocking the request.
     */
    public RequestBlockedException(final String reqUrl, final String remoteAddr, final String userAgent, final String message)
    {
        this.requestUrl = reqUrl;
        this.remoteAddress = remoteAddr;
        this.userAgent = userAgent;
        this.reason = message;
        return;
    }

    /**
     * Gets the requested URL.
     *
     * @return A URL.
     */
    public String getRequestUrl()
    {
        return this.requestUrl;
    }

    /**
     * Gets the remote address of the client making the request.
     *
     * @return A remote address.
     */
    public String getRemoteAddress()
    {
        return this.remoteAddress;
    }

    /**
     * Gets the user agent supplied by the client making the request.
     *
     * @return  A user agent string.
     */
    public String getUserAgent()
    {
        return this.userAgent;
    }

    /**
     * Gets the reason for blocking the request.
     *
     * @return  A message or reason for blocking the request.
     */
    public String getReason()
    {
        return this.reason;
    }
}

FirewallInterceptor.java

This interceptor is invoked after the Spring Security filters have run (i.e., after AnnotatingHttpFirewall has flagged requests that should be rejected. This interceptor detects those flags (attributes) on the request and raises a custom exception that our Error Controller can handle.

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/**
 * Intercepts requests that were flagged as rejected by the firewall.
 */
public final class FirewallInterceptor implements HandlerInterceptor
{
    /**
     * Default constructor.
     */
    public FirewallInterceptor()
    {
        return;
    }

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception
    {
        if (Boolean.TRUE.equals(request.getAttribute(AnnotatingHttpFirewall.HTTP_HEADER_REQUEST_REJECTED)))
        {
            // Throw a custom exception that can be handled by a custom error controller.
            final String reason = (String) request.getAttribute(AnnotatingHttpFirewall.HTTP_HEADER_REQUEST_REJECTED_REASON);
            throw new RequestRejectedByFirewallException(request.getRequestURL().toString(), request.getRemoteAddr(), request.getHeader(HttpHeaders.USER_AGENT), reason);
        }

        return true; // Allow the request to proceed normally.
    }

    @Override
    public void postHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final ModelAndView modelAndView) throws Exception
    {
        return;
    }

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception
    {
        return;
    }
}

WebConfig.java

In WebConfig, add the FirewallInterceptor to the registry.

@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter
{
    /**
     * Among your other methods in this class, make sure you register
     * your Interceptor.
     */
    @Override
    public void addInterceptors(final InterceptorRegistry registry)
    {
        // Register firewall interceptor for all URLs in webapp.
        registry.addInterceptor(new FirewallInterceptor()).addPathPatterns("/**");
        return;
    }
}

ErrorController.java

This specifically handles the custom exception above, and produces a clean error page for the client while logging all of the relevant information and invoking any special business logic for a custom application firewall.

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.springframework.web.servlet.NoHandlerFoundException;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import RequestBlockedException;

@ControllerAdvice
public final class ErrorController
{
    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(ErrorController.class.getName());

    /**
     * Generates an Error page by intercepting exceptions generated from AnnotatingHttpFirewall.
     *
     * @param request The original HTTP request.
     * @param ex A RequestBlockedException exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(RequestBlockedException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleRequestBlockedException(final RequestBlockedException ex)
    {
        if (LOGGER.isLoggable(Level.WARNING))
        {
            LOGGER.log(Level.WARNING, "Rejected request from " + ex.getRemoteAddress() + " for [" + ex.getRequestUrl() + "]. Reason: " + ex.getReason());
        }

        // Note: Perform any additional business logic or logging here.

        return "errorPage"; // Returns a nice error page with the specified status code.
    }

    /**
     * Generates a Page Not Found page.
     *
     * @param ex A NoHandlerFound exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String handleException(final NoHandlerFoundException ex)
    {
        return "notFoundPage";
    }
}

FirewallController.java

A controller with a default mapping that throws a NoHandlerFoundException. This circumvents the chicken-and-egg strategy in DispatcherServlet.noHandlerFound, allowing that method to always find a mapping so that FirewallInterceptor.preHandle is always invoked. This gives RequestRejectedByFirewallException priority over NoHandlerFoundException.

Why this is necessary:

As mentioned here, when a NoHandlerFoundException is thrown from DispatcherServlet (i.e., when a requested URL has no corresponding mapping), there is no way to handle the exceptions generated from the above firewall (NoHandlerFoundException is thrown prior to invoking preHandle()), so those requests will fall through to your 404 view (which is not the desired behavior in my case - you will see a lot of "No mapping found for HTTP request with URI..." messages). This could be fixed by moving the check for the special header into the noHandlerFound method. Unfortunately, there is no way to do this without writing a new Dispatcher Servlet from scratch, and then you may as well throw out the entire Spring Framework. It is impossible to extend DispatcherServlet due to the mix of protected, private and final methods, and the fact that its properties are inaccessible (no getters or setters). It is also impossible to wrap the class because there is no common interface that can be implemented. The default mapping in this class provides an elegant way to circumvent all of that logic.

Important caveat: The RequestMapping below will prevent resolution of static resources because it takes precedence over all registered ResourceHandlers. I am still looking for a workaround for this, but one possibility might be to try one of the methods for handling static resources suggested in this answer.

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.NoHandlerFoundException;

@Controller
public final class FirewallController
{
    /**
     * The name of the model attribute (or request parameter for advertisement click tracking) that contains the request URL.
     */
    protected static final String REQUEST_URL = "requestUrl";

    /**
     * The name of the model attribute that contains the request method.
     */
    protected static final String REQUEST_METHOD = "requestMethod";

    /**
     * The name of the model attribute that contains all HTTP headers.
     */
    protected static final String REQUEST_HEADERS = "requestHeaders";

    /**
     * Default constructor.
     */
    public FirewallController()
    {
        return;
    }

    /**
     * Populates the request URL model attribute from the HTTP request.
     *
     * @param request The HTTP request.
     * @return The request URL.
     */
    @ModelAttribute(REQUEST_URL)
    public final String getRequestURL(final HttpServletRequest request)
    {
        return request.getRequestURL().toString();
    }

    /**
     * Populates the request method from the HTTP request.
     *
     * @param request The HTTP request.
     * @return The request method (GET, POST, HEAD, etc.).
     */
    @ModelAttribute(REQUEST_METHOD)
    public final String getRequestMethod(final HttpServletRequest request)
    {
        return request.getMethod();
    }

    /**
     * Gets all headers from the HTTP request.
     *
     * @param request The HTTP request.
     * @return The request headers.
     */
    @ModelAttribute(REQUEST_HEADERS)
    public final HttpHeaders getRequestHeaders(final HttpServletRequest request)
    {
        return FirewallController.headers(request);
    }

    /**
     * A catch-all default mapping that throws a NoHandlerFoundException.
     * This will be intercepted by the ErrorController, which allows preHandle to work normally.
     *
     * @param requestMethod The request method.
     * @param requestUrl The request URL.
     * @param requestHeaders The request headers.
     * @throws NoHandlerFoundException every time this method is invoked.
     */
    @RequestMapping(value = "/**") // NOTE: This prevents resolution of static resources. Still looking for a workaround for this.
    public void getNotFoundPage(@ModelAttribute(REQUEST_METHOD) final String requestMethod, @ModelAttribute(REQUEST_URL) final String requestUrl, @ModelAttribute(REQUEST_HEADERS) final HttpHeaders requestHeaders) throws NoHandlerFoundException
    {
        throw new NoHandlerFoundException(requestMethod, requestUrl, requestHeaders);
    }

    /**
     * Gets all headers from a HTTP request.
     *
     * @param request The HTTP request.
     * @return The request headers.
     */
    public static HttpHeaders headers(final HttpServletRequest request)
    {
        final HttpHeaders headers = new HttpHeaders();

        for (Enumeration<?> names = request.getHeaderNames(); names.hasMoreElements();)
        {
            final String headerName = (String) names.nextElement();

            for (Enumeration<?> headerValues = request.getHeaders(headerName); headerValues.hasMoreElements();)
            {
                headers.add(headerName, (String) headerValues.nextElement());
            }
        }

        return headers;
    }
}

Results

When both parts of this are working, you'll see the following two warnings logged (the first one is in Spring Security, the second one is the Spring Framework (Core) ErrorController). Now you have full control over logging, and an extensible application firewall that you can adjust however you need.

Sep 12, 2018 10:24:37 AM com.mycompany.spring.security.AnnotatingHttpFirewall getFirewalledRequest
WARNING: Intercepted org.springframework.security.web.firewall.RequestRejectedException: Remote Host: 0:0:0:0:0:0:0:1 User Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0 Request URL: http://localhost:8080/webapp-www-mycompany-com/login.php
Sep 12, 2018 10:24:37 AM com.mycompany.spring.controller.ErrorController handleException
WARNING: Rejected request from 0:0:0:0:0:0:0:1 for [http://localhost:8080/webapp-www-mycompany-com/login.php]. Reason: The request was rejected because it is a likely vulnerability scan.
Seam answered 11/8, 2018 at 15:5 Comment(2)
you don't need to return in a void method. :)Avocado
For all of the other solutions proposed, I'd like to note that this method has been working flawlessly for five years with no changes.Seam
G
66

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);
        }
    }
}
Gainor answered 3/10, 2018 at 21:2 Comment(5)
This is a very unappreciated answer. Thanks for the simple solution!Selfdenial
This works but we are getting error logs instead of just warn logsMagma
Simple, effective. Thanks for sharing!Heart
This worked for me except I had to change the log method otherwise the stack trace was still written. Changed to: log.warn("request_rejected: remote={}, user_agent={}, request_url={} reason={}", request.getRemoteHost(), request.getHeader(HttpHeaders.USER_AGENT), request.getRequestURL(), e.getMessage());Thalia
For me sendError() triggered the app's 404 page. I didn't want that (because 99% of these requests are script kiddie attacks/probes), so I used setStatus(SC_NOT_FOUND) instead to just return an empty 404Amagasaki
C
39

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();
}
Choate answered 5/5, 2020 at 7:34 Comment(4)
Seems like RequestRejectedHandler is added only in spring security 5.4: github.com/spring-projects/spring-security/blob/master/web/src/… @since 5.4Plastered
@Plastered Yes, you're right, I will adapt it. Thank you!Choate
Thanks, this made short work of the issue. For me I just did an in-line implementation of the response class and use our own logging methods.Kugler
I was able to override the handle method to log the error and redirect to my 404 page. I posted the code as another answerRecant
S
18

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.

Seam answered 10/8, 2018 at 18:40 Comment(1)
This is still an issue. Hundreds of lines of pointless stack trace...Arroba
S
14

It turns out that although HttpFirewall and StrictHttpFirewall contain several design errors (documented in the code below), it is just barely possible to escape Spring Security's One True Firewall and tunnel the HttpFirewall information via a request attribute to a HandlerInterceptor that can pass these flagged requests to a real (persistent) firewall without sacrificing the original business logic that flagged them in the first place. The method documented here should be fairly future-proof, as it conforms to a simple contract from the HttpFirewall interface, and the rest is simply the core Spring Framework and Java Servlet API.

This is essentially a more complicated but more complete alternative to my earlier answer. In this answer, I implemented a new subclass of StrictHttpFirewall that intercepts and logs rejected requests at a specific logging level, but also adds an attribute to the HTTP request that flags it for downstream filters (or controllers) to handle. Also, this AnnotatingHttpFirewall provides an inspect() method that allows subclasses to add custom rules for blocking requests.

This solution is split into two parts: (1) Spring Security and (2) Spring Framework (Core), because that is the divide that caused this problem in the first place, and this shows how to bridge it.

For reference, this is tested on Spring 4.3.17 and Spring Security 4.2.6. There may be significant changes when Spring 5.1 is released.


Part 1: Spring Security

This is the half of the solution that performs the logging and flagging within Spring Security.


AnnotatingHttpFirewall.java

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 class AnnotatingHttpFirewall extends StrictHttpFirewall
{
    /**
     * The name of the HTTP header representing a request that has been rejected by this firewall.
     */
    public static final String HTTP_HEADER_REQUEST_REJECTED_FLAG = "X-HttpFirewall-RequestRejectedFlag";

    /**
     * The name of the HTTP header representing the reason a request has been rejected by this firewall.
     */
    public static final String HTTP_HEADER_REQUEST_REJECTED_REASON = "X-HttpFirewall-RequestRejectedReason";

    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(AnnotatingHttpFirewall.class.getName());

    /**
     * Default constructor.
     */
    public AnnotatingHttpFirewall()
    {
        super();
        return;
    }

    /**
     * Provides the request object which will be passed through the filter chain.
     *
     * @param request The original HttpServletRequest.
     * @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.
     */
    @Override
    public FirewalledRequest getFirewalledRequest(final HttpServletRequest request)
    {
        try
        {
            this.inspect(request); // Perform any additional checks that the naive "StrictHttpFirewall" misses.
            return super.getFirewalledRequest(request);
        } catch (RequestRejectedException ex) {
            final String requestUrl = request.getRequestURL().toString();

            // Override some of the default behavior because some requests are
            // legitimate.
            if (requestUrl.contains(";jsessionid="))
            {
                // Do not block non-cookie serialized sessions. Google's crawler does this often.
            } else {
                // Log anything that is blocked so we can find these in the catalina.out log.
                // This will give us any information we need to make
                // adjustments to these special cases and see potentially
                // malicious activity.
                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());
                }

                // Mark this request as rejected.
                request.setAttribute(HTTP_HEADER_REQUEST_REJECTED, Boolean.TRUE);
                request.setAttribute(HTTP_HEADER_REQUEST_REJECTED_REASON, ex.getMessage());
            }

            // Suppress the RequestBlockedException and pass the request through
            // with the additional attribute.
            return new FirewalledRequest(request)
            {
                @Override
                public void reset()
                {
                    return;
                }
            };
        }
    }

    /**
     * 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);
    }

    /**
     * Perform any custom checks on the request.
     * This method may be overridden by a subclass in order to supplement or replace these tests.
     *
     * @param request The original HttpServletRequest.
     * @throws RequestRejectedException if the request should be rejected immediately.
     */
    public void inspect(final HttpServletRequest request) throws RequestRejectedException
    {
        final String requestUri = request.getRequestURI(); // path without parameters
//        final String requestUrl = request.getRequestURL().toString(); // full path with parameters

        if (requestUri.endsWith("/wp-login.php"))
        {
            throw new RequestRejectedException("The request was rejected because it is a vulnerability scan.");
        }

        if (requestUri.endsWith(".php"))
        {
            throw new RequestRejectedException("The request was rejected because it is a likely vulnerability scan.");
        }

        return; // The request passed all custom tests.
    }
}

WebSecurityConfig.java

In WebSecurityConfig, set the HTTP firewall to the AnnotatingHttpFirewall.

@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 AnnotatingHttpFirewall()); // Set the custom firewall.
        return;
    }
}

Part 2: Spring Framework

The second part of this solution could conceivably be implemented as a ServletFilter or HandlerInterceptor. I'm going the path of a HandlerInterceptor because it seems to give the most flexibility and works directly within the Spring Framework.


RequestBlockedException.java

This custom exception can be handled by an Error Controller. This may be extended to add any request headers, parameters or properties available from the raw request (even the full request itself) that may be pertinent to application business logic (e.g., a persistent firewall).

/**
 * A custom exception for situations where a request is blocked or rejected.
 */
public class RequestBlockedException extends RuntimeException
{
    private static final long serialVersionUID = 1L;

    /**
     * The requested URL.
     */
    private String requestUrl;

    /**
     * The remote address of the client making the request.
     */
    private String remoteAddress;

    /**
     * A message or reason for blocking the request.
     */
    private String reason;

    /**
     * The user agent supplied by the client the request.
     */
    private String userAgent;

    /**
     * Creates a new Request Blocked Exception.
     *
     * @param reqUrl The requested URL.
     * @param remoteAddr The remote address of the client making the request.
     * @param userAgent The user agent supplied by the client making the request.
     * @param message A message or reason for blocking the request.
     */
    public RequestBlockedException(final String reqUrl, final String remoteAddr, final String userAgent, final String message)
    {
        this.requestUrl = reqUrl;
        this.remoteAddress = remoteAddr;
        this.userAgent = userAgent;
        this.reason = message;
        return;
    }

    /**
     * Gets the requested URL.
     *
     * @return A URL.
     */
    public String getRequestUrl()
    {
        return this.requestUrl;
    }

    /**
     * Gets the remote address of the client making the request.
     *
     * @return A remote address.
     */
    public String getRemoteAddress()
    {
        return this.remoteAddress;
    }

    /**
     * Gets the user agent supplied by the client making the request.
     *
     * @return  A user agent string.
     */
    public String getUserAgent()
    {
        return this.userAgent;
    }

    /**
     * Gets the reason for blocking the request.
     *
     * @return  A message or reason for blocking the request.
     */
    public String getReason()
    {
        return this.reason;
    }
}

FirewallInterceptor.java

This interceptor is invoked after the Spring Security filters have run (i.e., after AnnotatingHttpFirewall has flagged requests that should be rejected. This interceptor detects those flags (attributes) on the request and raises a custom exception that our Error Controller can handle.

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/**
 * Intercepts requests that were flagged as rejected by the firewall.
 */
public final class FirewallInterceptor implements HandlerInterceptor
{
    /**
     * Default constructor.
     */
    public FirewallInterceptor()
    {
        return;
    }

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception
    {
        if (Boolean.TRUE.equals(request.getAttribute(AnnotatingHttpFirewall.HTTP_HEADER_REQUEST_REJECTED)))
        {
            // Throw a custom exception that can be handled by a custom error controller.
            final String reason = (String) request.getAttribute(AnnotatingHttpFirewall.HTTP_HEADER_REQUEST_REJECTED_REASON);
            throw new RequestRejectedByFirewallException(request.getRequestURL().toString(), request.getRemoteAddr(), request.getHeader(HttpHeaders.USER_AGENT), reason);
        }

        return true; // Allow the request to proceed normally.
    }

    @Override
    public void postHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final ModelAndView modelAndView) throws Exception
    {
        return;
    }

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception
    {
        return;
    }
}

WebConfig.java

In WebConfig, add the FirewallInterceptor to the registry.

@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter
{
    /**
     * Among your other methods in this class, make sure you register
     * your Interceptor.
     */
    @Override
    public void addInterceptors(final InterceptorRegistry registry)
    {
        // Register firewall interceptor for all URLs in webapp.
        registry.addInterceptor(new FirewallInterceptor()).addPathPatterns("/**");
        return;
    }
}

ErrorController.java

This specifically handles the custom exception above, and produces a clean error page for the client while logging all of the relevant information and invoking any special business logic for a custom application firewall.

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.springframework.web.servlet.NoHandlerFoundException;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import RequestBlockedException;

@ControllerAdvice
public final class ErrorController
{
    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(ErrorController.class.getName());

    /**
     * Generates an Error page by intercepting exceptions generated from AnnotatingHttpFirewall.
     *
     * @param request The original HTTP request.
     * @param ex A RequestBlockedException exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(RequestBlockedException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleRequestBlockedException(final RequestBlockedException ex)
    {
        if (LOGGER.isLoggable(Level.WARNING))
        {
            LOGGER.log(Level.WARNING, "Rejected request from " + ex.getRemoteAddress() + " for [" + ex.getRequestUrl() + "]. Reason: " + ex.getReason());
        }

        // Note: Perform any additional business logic or logging here.

        return "errorPage"; // Returns a nice error page with the specified status code.
    }

    /**
     * Generates a Page Not Found page.
     *
     * @param ex A NoHandlerFound exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String handleException(final NoHandlerFoundException ex)
    {
        return "notFoundPage";
    }
}

FirewallController.java

A controller with a default mapping that throws a NoHandlerFoundException. This circumvents the chicken-and-egg strategy in DispatcherServlet.noHandlerFound, allowing that method to always find a mapping so that FirewallInterceptor.preHandle is always invoked. This gives RequestRejectedByFirewallException priority over NoHandlerFoundException.

Why this is necessary:

As mentioned here, when a NoHandlerFoundException is thrown from DispatcherServlet (i.e., when a requested URL has no corresponding mapping), there is no way to handle the exceptions generated from the above firewall (NoHandlerFoundException is thrown prior to invoking preHandle()), so those requests will fall through to your 404 view (which is not the desired behavior in my case - you will see a lot of "No mapping found for HTTP request with URI..." messages). This could be fixed by moving the check for the special header into the noHandlerFound method. Unfortunately, there is no way to do this without writing a new Dispatcher Servlet from scratch, and then you may as well throw out the entire Spring Framework. It is impossible to extend DispatcherServlet due to the mix of protected, private and final methods, and the fact that its properties are inaccessible (no getters or setters). It is also impossible to wrap the class because there is no common interface that can be implemented. The default mapping in this class provides an elegant way to circumvent all of that logic.

Important caveat: The RequestMapping below will prevent resolution of static resources because it takes precedence over all registered ResourceHandlers. I am still looking for a workaround for this, but one possibility might be to try one of the methods for handling static resources suggested in this answer.

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.NoHandlerFoundException;

@Controller
public final class FirewallController
{
    /**
     * The name of the model attribute (or request parameter for advertisement click tracking) that contains the request URL.
     */
    protected static final String REQUEST_URL = "requestUrl";

    /**
     * The name of the model attribute that contains the request method.
     */
    protected static final String REQUEST_METHOD = "requestMethod";

    /**
     * The name of the model attribute that contains all HTTP headers.
     */
    protected static final String REQUEST_HEADERS = "requestHeaders";

    /**
     * Default constructor.
     */
    public FirewallController()
    {
        return;
    }

    /**
     * Populates the request URL model attribute from the HTTP request.
     *
     * @param request The HTTP request.
     * @return The request URL.
     */
    @ModelAttribute(REQUEST_URL)
    public final String getRequestURL(final HttpServletRequest request)
    {
        return request.getRequestURL().toString();
    }

    /**
     * Populates the request method from the HTTP request.
     *
     * @param request The HTTP request.
     * @return The request method (GET, POST, HEAD, etc.).
     */
    @ModelAttribute(REQUEST_METHOD)
    public final String getRequestMethod(final HttpServletRequest request)
    {
        return request.getMethod();
    }

    /**
     * Gets all headers from the HTTP request.
     *
     * @param request The HTTP request.
     * @return The request headers.
     */
    @ModelAttribute(REQUEST_HEADERS)
    public final HttpHeaders getRequestHeaders(final HttpServletRequest request)
    {
        return FirewallController.headers(request);
    }

    /**
     * A catch-all default mapping that throws a NoHandlerFoundException.
     * This will be intercepted by the ErrorController, which allows preHandle to work normally.
     *
     * @param requestMethod The request method.
     * @param requestUrl The request URL.
     * @param requestHeaders The request headers.
     * @throws NoHandlerFoundException every time this method is invoked.
     */
    @RequestMapping(value = "/**") // NOTE: This prevents resolution of static resources. Still looking for a workaround for this.
    public void getNotFoundPage(@ModelAttribute(REQUEST_METHOD) final String requestMethod, @ModelAttribute(REQUEST_URL) final String requestUrl, @ModelAttribute(REQUEST_HEADERS) final HttpHeaders requestHeaders) throws NoHandlerFoundException
    {
        throw new NoHandlerFoundException(requestMethod, requestUrl, requestHeaders);
    }

    /**
     * Gets all headers from a HTTP request.
     *
     * @param request The HTTP request.
     * @return The request headers.
     */
    public static HttpHeaders headers(final HttpServletRequest request)
    {
        final HttpHeaders headers = new HttpHeaders();

        for (Enumeration<?> names = request.getHeaderNames(); names.hasMoreElements();)
        {
            final String headerName = (String) names.nextElement();

            for (Enumeration<?> headerValues = request.getHeaders(headerName); headerValues.hasMoreElements();)
            {
                headers.add(headerName, (String) headerValues.nextElement());
            }
        }

        return headers;
    }
}

Results

When both parts of this are working, you'll see the following two warnings logged (the first one is in Spring Security, the second one is the Spring Framework (Core) ErrorController). Now you have full control over logging, and an extensible application firewall that you can adjust however you need.

Sep 12, 2018 10:24:37 AM com.mycompany.spring.security.AnnotatingHttpFirewall getFirewalledRequest
WARNING: Intercepted org.springframework.security.web.firewall.RequestRejectedException: Remote Host: 0:0:0:0:0:0:0:1 User Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0 Request URL: http://localhost:8080/webapp-www-mycompany-com/login.php
Sep 12, 2018 10:24:37 AM com.mycompany.spring.controller.ErrorController handleException
WARNING: Rejected request from 0:0:0:0:0:0:0:1 for [http://localhost:8080/webapp-www-mycompany-com/login.php]. Reason: The request was rejected because it is a likely vulnerability scan.
Seam answered 11/8, 2018 at 15:5 Comment(2)
you don't need to return in a void method. :)Avocado
For all of the other solutions proposed, I'd like to note that this method has been working flawlessly for five years with no changes.Seam
C
3

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);
        }
    }
}
Choate answered 29/3, 2019 at 15:26 Comment(3)
This is invoked twiceMagma
@SabareeshKkanan this is due to the default advice invocation of AspectJ advices. It is correct that the handleRequestRejectedException() method is invoked twice, but the actual method invocation of the FilterChainProxy.doFilter() is called only once. Take a look here to understand more how the @Around interception works.Choate
seems like doesn't work on Spring 5.2.9, NPE is thrown out of GenericFilterBean#init method, during context setup, since Log logger field is null for some unknown reasonsCanter
R
2

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.

Reformer answered 27/8, 2020 at 15:41 Comment(1)
Thanks for the update. I'll hold off on migrating to Spring 5 until this is included in a release. In the meantime, my above strategy (see accepted solution) has been working flawlessly for over 2 years.Seam
R
1

In spring security 5.7.6 I was able to use this code to log the error and redirect to the 404 page

@Bean
public RequestRejectedHandler requestRejectedHandler() {
    HttpStatusRequestRejectedHandler rejectedHandler = new HttpStatusRequestRejectedHandler() {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, RequestRejectedException requestRejectedException) throws IOException {
            log.info(requestRejectedException.getMessage() + " for " + request.getRequestURL());
            response.sendRedirect("/404");
        }
    };
    return rejectedHandler;
}
Recant answered 3/2, 2023 at 17:6 Comment(0)
P
1

Easiest ootb solution is to create a bean of RequestRejectedHandler and do what you want with that exception:

@Bean
public RequestRejectedHandler handler() {
    return (request, response, exception) -> response.sendError(HttpStatus.FORBIDDEN.value());
}

In above case this exception will cause in 403 http response status. In this place you can additionally add your logger (logger.warn(...)), send redirection to error page (response.sendRedirect("/500")), set the headers etc.

Phlyctena answered 4/8, 2023 at 10:1 Comment(0)
A
0

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;
}
Accused answered 20/11, 2019 at 10:22 Comment(0)
H
0

Our's was spring-webmvc (4.3.25.RELEASE) GUI using spring-security-core (4.2.13.RELEASE) and the issue was due to url string containing ";jsessionid=D3A0470674704B75756AA10F50AA2CFC" with semicolon as one of its parameters.

The error org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";" use to occur only when web page loads for the first time causing all sorts of CSS format issue with images not getting loaded, color and fonts not being set properly. However, after refresh of the same page or clicking any of the navigation links the next page use to load fine with all CSS implemented properly. Also, this error RequestRejectedException badly polluted the logs.

We wanted to handle this issue such that when a new session is created, the generated cookie handles the session with jsessionid and not the query string like how it does for the 2nd time onwards.

My solution was derived from implementing the filter as described above in https://mcmap.net/q/18110/-how-to-intercept-a-requestrejectedexception-in-spring but instead of sending it to an error page check for session and redirect it to encoded URL as described in https://mcmap.net/q/18295/-jsessionid-is-occurred-in-all-urls-which-are-generated-by-jstl-lt-c-url-gt-tag. After this solution we never received the RequestRejectedException or CSS issue even for new sessions or first time page load.

The updated LogAndSuppressRequestRejectedExceptionFilter can be seen below

@Component("logAndSuppressRequestRejectedExceptionFilter")
@Order(Ordered.HIGHEST_PRECEDENCE)
public class LogAndSuppressRequestRejectedExceptionFilter extends GenericFilterBean {
    private static final Logger logger = LoggerFactory.getLogger(LogAndSuppressRequestRejectedExceptionFilter.class);

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        try {
            HttpServletRequest httpRequest = (HttpServletRequest) req;
            HttpServletResponse httpResponse = (HttpServletResponse) res;
            HttpSession session = httpRequest.getSession();

            if (session.isNew()) {
                // New session? OK, redirect to encoded URL with jsessionid in it (and
                // implicitly also set cookie).
                logger.debug("New session - redirect to encoded url");
                httpResponse.sendRedirect(httpResponse.encodeRedirectURL(httpRequest.getRequestURI()));
                return;
            } else if (session.getAttribute("verified") == null) {
                // Session has not been verified yet? OK, mark it verified so that we don't need
                // to repeat this.
                logger.debug("Setting session to verified");
                session.setAttribute("verified", true);
                if (httpRequest.isRequestedSessionIdFromCookie()) {
                    // Supports cookies? OK, redirect to unencoded URL to get rid of jsessionid in
                    // URL.
                    logger.debug("redirect to unencoded URL to get rid of jsessionid in url");
                    httpResponse.sendRedirect(httpRequest.getRequestURI().split(";")[0]);
                    return;
                }
            }

            chain.doFilter(req, res);

        } catch (RequestRejectedException ex) {
            HttpServletRequest request = (HttpServletRequest) req;
            logger.warn("request_rejected: remote={}, user_agent={}, request_url={}", request.getRemoteHost(),
                    request.getHeader(HttpHeaders.USER_AGENT), request.getRequestURL(), ex.getMessage());
            return;
        }
    }
}
Hennebery answered 17/9, 2021 at 4:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.