如何在Spring中拦截RequestRejectedException?
我看到 一吨
的RequestRejectedException
条目在我的Tomcat日志(样品粘贴下面)。这些在几个月前进行次要版本升级(Spring
Security
4.2.4,IIRC)后开始出现在我的日志文件中,因此,这显然是默认情况下启用的Spring新安全功能。这里报告了类似的问题,但是我的问题专门涉及如何在控制器中拦截这些异常。有一个针对此问题的Spring
Security错误记录(提供一种处理RequestRejectedException的方法)。但是,直到Spring 5.1,他们才针对此问题进行修复。
我了解为什么会引发这些异常,并且我不想禁用此安全功能。
我想对该功能进行一些控制,以便:
- 我知道我不会阻止合法用户访问我的网站。
- 我可以看到哪些请求触发了此请求(它们是SQL注入攻击吗?)
- 我可以调整服务器响应。Spring Security防火墙将一个完整的堆栈跟踪以及一个
500 Internal Server Error
(这是非常错误的,应该是400 Bad Request
)转储到Web客户端(信息泄露)。
我想找到一种方法来记录所请求的URL,同时还要专门针对这些异常取消堆栈跟踪,因为这些异常会污染我的日志文件而没有提供任何有用的信息。理想情况下,我想拦截这些异常并在我的应用程序层中处理它们,而不是根本不在Tomcat日志中报告它们。
例如,这是每天在my中出现的数千种日志条目之一catalina.out
:
Aug 10, 2018 2:01:36 PM org.apache.catalina.core.StandardWrapperValve invokeSEVERE: 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)
我在两天内看到了3200多个catalina.out
日志,并且它很快成为日志文件的最大贡献者,以至于它阻止了我看到其他合理的问题。本质上,这项新的Spring
Security功能是内置的“拒绝服务”的一种形式,自4月以来浪费了我很多时间。我并不是说这不是一个重要功能,只是默认实现完全被破坏了,我想找到一种方法来以开发人员和系统管理员的身份对其进行某种控制。
我使用一个自定义的错误控制器来拦截IOException
Spring中的许多其他异常类型(包括)。但是,RequestRejectedException
似乎由于某种原因而失败了。
这是我的相关部分ErrorController.java
,可以让我大致了解要完成的工作:
@ControllerAdvicepublic 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";
}
}
此错误控制器适用于许多异常。例如,它成功拦截了此IllegalStateException
:
Aug 05, 2018 7:50:30 AM com.mycompany.spring.controller.ErrorController handleExceptionSEVERE: 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)
...
但是,这不是拦截RequestRejectedException
(如上述第一个日志示例中缺少“服务器错误”所示)。
如何拦截RequestRejectedException
错误控制器?
回答:
事实证明,尽管HttpFirewall
其中StrictHttpFirewall
包含多个设计错误(在下面的代码中进行了记录),但是几乎不可能逃避Spring
Security的 One True Firewall
并HttpFirewall
通过request属性将信息HandlerInterceptor
传递到,该属性可以将这些标记的请求传递给 真实的
(持久性)防火墙,而不会牺牲最初标记它们的原始业务逻辑。这里记录的方法应该相当面向未来,因为它符合来自HttpFirewall
接口的简单协定,其余的只是核心Spring
Framework和Java Servlet API。
从本质上讲,这是我先前回答的更复杂但更完整的选择。在此答案中,我实现了一个新的子类,StrictHttpFirewall
该子类在特定的日志记录级别拦截并记录被拒绝的请求,而且还向HTTP请求添加了一个属性,将该属性标记为供下游过滤器(或控制器)处理。此外,这AnnotatingHttpFirewall
提供了一种inspect()
方法,该方法允许子类添加用于阻止请求的自定义规则。
该解决方案分为两部分:(1) Spring Security 和(2) Spring Framework(Core)
,因为这首先是导致此问题的原因,并且说明了如何解决该问题。
作为参考,已在Spring 4.3.17和Spring Security 4.2.6上进行了测试。Spring 5.1发布时可能会有重大变化。
这是在Spring Security中执行日志记录和标记的解决方案的一半。
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
,将HTTP防火墙设置为AnnotatingHttpFirewall
。
@EnableWebSecuritypublic 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;
}
}
可以想象,该解决方案的第二部分可以实现为ServletFilter
或HandlerInterceptor
。我HandlerInterceptor
之所以走a之路,是因为它似乎提供了最大的灵活性,并且可以直接在Spring
Framework中工作。
此自定义异常可以由错误控制器处理。可以扩展它以添加可与应用程序业务逻辑(例如,持久防火墙)相关的原始请求(甚至是完整请求本身)中可用的任何请求标头,参数或属性。
/** * 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;
}
}
在Spring
Security过滤器运行之后(即,在AnnotatingHttpFirewall
标记了应拒绝的请求之后)将调用此拦截器。此拦截器将检测请求上的那些标志(属性),并引发我们的错误控制器可以处理的自定义异常。
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
,将添加FirewallInterceptor
到注册表。
@EnableWebMvcpublic 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;
}
}
这将专门处理上述自定义异常,并在记录所有相关信息并为自定义应用程序防火墙调用任何特殊的业务逻辑时,为客户端生成一个干净的错误页面。
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";
}
}
具有默认映射并抛出的控制器NoHandlerFoundException
。这规避了DispatcherServlet.noHandlerFound中的先有其后的策略,允许该方法
始终
查找映射,以便FirewallInterceptor.preHandle
始终对其进行调用。这使RequestRejectedByFirewallException
优先NoHandlerFoundException
。
为什么需要这样做:
如所提到的在这里,当NoHandlerFoundException
从抛出DispatcherServlet
(即,当一个请求的URL不具有相应的映射),也没有办法来处理从上面的防火墙生成的异常(NoHandlerFoundException
被调用之前抛出preHandle()
,所以这些请求会)进入404视图(在我的情况下,这不是您想要的行为-
您会看到很多“找不到使用URI的HTTP请求的映射…”消息)。可以通过将特殊标头的检查移到noHandlerFound
方法中来解决此问题。不幸的是,如果没有从头开始编写新的Dispatcher
Servlet,就无法做到这一点,那么您最好扔掉整个Spring框架。不可能扩展DispatcherServlet
由于受保护的方法,私有方法和最终方法的混合使用,并且其属性不可访问(无getter或setter)。包装类也是不可能的,因为没有可以实现的通用接口。此类中的默认映射提供了一种优雅的方式来规避所有这些逻辑。
:下面的RequestMapping将阻止解析静态资源,因为它优先于所有已注册的ResourceHandler。我仍在寻找解决方法,但是一种可能是尝试使用此答案中建议的处理静态资源的方法之一。
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;
}
}
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 getFirewalledRequestWARNING: 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.
以上是 如何在Spring中拦截RequestRejectedException? 的全部内容, 来源链接: utcz.com/qa/430049.html