如何拦截 Spring 中的 RequestRejectedException?
How to intercept a RequestRejectedException in Spring?
我在 Tomcat 日志中看到 一吨 个 RequestRejectedException
条目(示例粘贴在下方)。这些在几个月前的次要版本升级(Spring Security 4.2.4,IIRC)后开始出现在我的日志文件中,所以这显然是 Spring 中默认启用的新安全功能。类似的问题是 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)。但是,在 Spring 5.1.
之前,他们不会针对此问题进行修复
我明白了why these exceptions are being thrown, and I do not want to disable this security feature。
我想获得对此功能的一些控制权,以便:
- 我知道我没有阻止合法用户访问我的站点。
- 我可以看到是什么请求触发了这个(它们是 SQL 注入攻击吗?)
- 我可以调整服务器响应。 Spring 安全防火墙将完整的堆栈跟踪转储到 Web 客户端(信息泄露),以及
500 Internal Server Error
(这是非常不正确的,这应该是 400 Bad Request
)。
我想找到一种方法来记录所请求的 URL,但同时也抑制专门针对这些异常的堆栈跟踪,因为它们污染了我的日志文件而没有给我任何有用的信息。最理想的是,我想拦截这些异常并在我的应用程序层中处理它们,而不是在 Tomcat 日志中报告它们。
例如,这是每天出现在我 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)
我在两天内看到了超过 3,200 个,它很快成为我的 catalina.out
日志文件的最大贡献者,以至于它阻止我看到其他合法的问题。从本质上讲,这个新的 Spring 安全功能是一种内置的拒绝服务形式,自 4 月以来它已经浪费了我数小时的时间。我并不是说它不是一个重要的特性,只是说默认实现完全是拙劣的,我想找到一种方法来控制它,无论是作为开发人员还是作为系统管理员。
我使用自定义错误控制器拦截 Spring 中的许多其他异常类型(包括 IOException
)。但是,RequestRejectedException
似乎由于某种原因而失败了。
这是我的 ErrorController.java
的相关部分,以说明我要完成的工作:
@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";
}
}
这个错误控制器适用于许多异常。比如成功拦截了这个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)
...
但是,这不是拦截 RequestRejectedException
(如上面第一个日志示例中缺少 "Server Error" 所示)。
如何在错误控制器中拦截 RequestRejectedException
?
我实现了 StrictHttpFirewall
的一个子 class,它将请求信息记录到控制台并抛出一个新的异常并抑制堆栈跟踪。这部分解决了我的问题(至少我现在可以看到错误的请求)。
如果您只想查看没有堆栈跟踪的被拒绝的请求,这就是您要找的答案。
如果您想在控制器中处理这些异常,请参阅以获得完整(但稍微复杂)的解决方案。
LoggingHttpFirewall.java
此 class 扩展 StrictHttpFirewall 以捕获 RequestRejectedException
并抛出一个新异常,其中包含来自请求的元数据和抑制的堆栈跟踪。
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
在 WebSecurityConfig
中,将 HTTP 防火墙设置为 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;
}
}
结果
将此解决方案部署到生产环境后,我很快发现 StrictHttpFirewall
的默认行为是阻止 Google 将我的站点编入索引!
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
我一发现这一点,就迅速部署了一个新版本(包含在 中)来查找 ;jsessionid=
并允许这些请求通过。很可能还有其他请求也应该通过,现在我有一种检测这些请求的方法。
事实证明,虽然 HttpFirewall
和 StrictHttpFirewall
包含几个设计错误(记录在下面的代码中),但几乎不可能逃脱 Spring 安全性的 一个 True Firewall 并通过请求属性将 HttpFirewall
信息隧道传送到 HandlerInterceptor
,后者可以将这些标记的请求传递给 real(持久) 防火墙而不牺牲最初标记它们的原始业务逻辑。这里记录的方法应该是相当future-proof,因为它符合HttpFirewall
接口的简单契约,剩下的只是核心Spring框架和JavaServletAPI.
这本质上是 my earlier answer 的更复杂但更完整的替代方法。在这个答案中,我实现了一个新的 StrictHttpFirewall
的子class,它拦截并记录特定日志记录级别的拒绝请求,但也向 HTTP 请求添加了一个属性,为下游过滤器(或控制器)标记它) 处理。此外,此 AnnotatingHttpFirewall
提供了一个 inspect()
方法,允许 subclasses 添加自定义规则来阻止请求。
此解决方案分为两部分:(1) Spring 安全性 和 (2) Spring 框架(核心) ,因为那是首先导致此问题的鸿沟,这说明了如何弥合它。
作为参考,这是在 Spring 4.3.17 和 Spring Security 4.2.6 上测试的。 Spring 5.1 发布时可能会有重大变化。
第 1 部分:Spring 安全性
这是在 Spring 安全性中执行日志记录和标记的解决方案的一半。
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
在 WebSecurityConfig
中,将 HTTP 防火墙设置为 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;
}
}
第 2 部分:Spring 框架
这个解决方案的第二部分可以想象地实现为 ServletFilter
或 HandlerInterceptor
。我要走 HandlerInterceptor
的道路,因为它似乎提供了最大的灵活性,并且可以直接在 Spring 框架内工作。
RequestBlockedException.java
此自定义异常可由错误控制器处理。这可能会扩展到添加任何请求 headers,参数或属性可用的原始请求(甚至是完整的请求本身)可能与应用程序业务逻辑相关(例如,持久防火墙)。
/**
* 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
此拦截器在 Spring 安全过滤器具有 运行 之后调用(即,在 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.java
在 WebConfig
中,将 FirewallInterceptor
添加到注册表。
@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
这专门处理上面的自定义异常,并为客户端生成一个干净的错误页面,同时记录所有相关信息并为自定义应用程序防火墙调用任何特殊业务逻辑。
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
具有抛出 NoHandlerFoundException
的默认映射的控制器。
这绕过了 DispatcherServlet.noHandlerFound 中的 chicken-and-egg 策略,允许该方法 始终 找到一个映射,以便始终调用 FirewallInterceptor.preHandle
。这使 RequestRejectedByFirewallException
优先于 NoHandlerFoundException
。
为什么这是必要的:
如前所述 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()),因此这些请求将进入您的 404 视图(在我的情况下这不是所需的行为 - 您会看到很多 "No mapping found for HTTP request with URI..." 消息)。这可以通过将特殊 header 的检查移动到 noHandlerFound
方法中来解决。不幸的是,如果不从头编写一个新的 Dispatcher Servlet 就没有办法做到这一点,那么你还不如扔掉整个 Spring Framework。由于受保护的、私有的和最终的方法的混合,以及它的属性不可访问的事实(没有 getter 或 setter),不可能扩展 DispatcherServlet
。也不可能把class包裹起来,因为没有可以实现的通用接口。此 class 中的默认映射提供了一种优雅的方式来规避所有这些逻辑。
重要提示:下面的 RequestMapping 将阻止解析静态资源,因为它优先于所有已注册的 ResourceHandler。我仍在为此寻找解决方法,但一种可能是尝试 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;
}
}
结果
当这两个部分都正常工作时,您会看到记录了以下两个警告(第一个在 Spring 安全中,第二个是 Spring 框架(核心)ErrorController
).现在您可以完全控制日志记录和可扩展的应用程序防火墙,您可以根据需要进行调整
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.
也可以通过简单的过滤器处理,会导致404错误响应
@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);
}
}
}
另一种处理方法是使用 Spring AOP. We can create an advice around the FilterChainProxy.doFilter() 方法捕获 HttpFirewall 抛出的任何 RequestRejectedException 并将其转换为 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);
}
}
}
一个非常简单的方法是使用web.xml;在该文件中指定一个错误页面:
<error-page>
<exception-type>org.springframework.security.web.firewall.RequestRejectedException</exception-type>
<location>/request-rejected</location>
</error-page>
对于指定的路径(位置),在@Controller
-注释中添加一个映射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;
}
对于 Spring 安全版本 5.4
及更高版本,您可以简单地创建一个 RequestRejectedHandler
类型的 bean,它将被注入 Spring 安全过滤器链
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();
}
我看到最近 github 在 this commit
中更改了一些可行的解决方案
如果您注册 RequestRejectedHandler
类型的 bean,它应该可以工作,或者如我所见,也将通过 WebSecurity
在 WebSecurityConfigurerAdapter
中进行集成。不幸的是,此更改可能未包含在 2.3.3.RELEASE 使用依赖项管理中。它应该出现在 Spring 安全配置 5.4.0-M1 中。依赖管理,版本为2.4.0-M1.
看到这个答案的人迟早会在标准版本中看到这一变化。
我们的是 spring-webmvc (4.3.25.RELEASE) GUI 使用 spring-security-core
(4.2.13.RELEASE) 问题是由于 url 字符串包含 ";jsessionid=D3A0470674704B75756AA10F50AA2CFC"
,分号作为其参数之一。
错误 org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
仅在首次加载网页时发生,导致各种 CSS 格式问题,图像未加载,颜色和字体设置不正确。但是,在刷新同一页面或单击任何导航链接后,下一页可以正常加载并正确实现所有 CSS。此外,此错误 RequestRejectedException
严重污染了日志。
我们想要处理这个问题,以便在创建新会话时,生成的 cookie 使用 jsessionid 处理会话,而不是像第二次以后那样处理查询字符串。
我的解决方案源自 but instead of sending it to an error page check for session and redirect it to encoded URL as described in 中上文所述的过滤器实施。在这个解决方案之后,我们从未收到 RequestRejectedException
或 CSS 问题,即使对于新会话或首次页面加载也是如此。
更新后的LogAndSuppressRequestRejectedExceptionFilter
如下图
@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;
}
}
}
我在 Tomcat 日志中看到 一吨 个 RequestRejectedException
条目(示例粘贴在下方)。这些在几个月前的次要版本升级(Spring Security 4.2.4,IIRC)后开始出现在我的日志文件中,所以这显然是 Spring 中默认启用的新安全功能。类似的问题是 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)。但是,在 Spring 5.1.
我明白了why these exceptions are being thrown, and I do not want to disable this security feature。
我想获得对此功能的一些控制权,以便:
- 我知道我没有阻止合法用户访问我的站点。
- 我可以看到是什么请求触发了这个(它们是 SQL 注入攻击吗?)
- 我可以调整服务器响应。 Spring 安全防火墙将完整的堆栈跟踪转储到 Web 客户端(信息泄露),以及
500 Internal Server Error
(这是非常不正确的,这应该是400 Bad Request
)。
我想找到一种方法来记录所请求的 URL,但同时也抑制专门针对这些异常的堆栈跟踪,因为它们污染了我的日志文件而没有给我任何有用的信息。最理想的是,我想拦截这些异常并在我的应用程序层中处理它们,而不是在 Tomcat 日志中报告它们。
例如,这是每天出现在我 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)
我在两天内看到了超过 3,200 个,它很快成为我的 catalina.out
日志文件的最大贡献者,以至于它阻止我看到其他合法的问题。从本质上讲,这个新的 Spring 安全功能是一种内置的拒绝服务形式,自 4 月以来它已经浪费了我数小时的时间。我并不是说它不是一个重要的特性,只是说默认实现完全是拙劣的,我想找到一种方法来控制它,无论是作为开发人员还是作为系统管理员。
我使用自定义错误控制器拦截 Spring 中的许多其他异常类型(包括 IOException
)。但是,RequestRejectedException
似乎由于某种原因而失败了。
这是我的 ErrorController.java
的相关部分,以说明我要完成的工作:
@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";
}
}
这个错误控制器适用于许多异常。比如成功拦截了这个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)
...
但是,这不是拦截 RequestRejectedException
(如上面第一个日志示例中缺少 "Server Error" 所示)。
如何在错误控制器中拦截 RequestRejectedException
?
我实现了 StrictHttpFirewall
的一个子 class,它将请求信息记录到控制台并抛出一个新的异常并抑制堆栈跟踪。这部分解决了我的问题(至少我现在可以看到错误的请求)。
如果您只想查看没有堆栈跟踪的被拒绝的请求,这就是您要找的答案。
如果您想在控制器中处理这些异常,请参阅
LoggingHttpFirewall.java
此 class 扩展 StrictHttpFirewall 以捕获 RequestRejectedException
并抛出一个新异常,其中包含来自请求的元数据和抑制的堆栈跟踪。
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
在 WebSecurityConfig
中,将 HTTP 防火墙设置为 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;
}
}
结果
将此解决方案部署到生产环境后,我很快发现 StrictHttpFirewall
的默认行为是阻止 Google 将我的站点编入索引!
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
我一发现这一点,就迅速部署了一个新版本(包含在 ;jsessionid=
并允许这些请求通过。很可能还有其他请求也应该通过,现在我有一种检测这些请求的方法。
事实证明,虽然 HttpFirewall
和 StrictHttpFirewall
包含几个设计错误(记录在下面的代码中),但几乎不可能逃脱 Spring 安全性的 一个 True Firewall 并通过请求属性将 HttpFirewall
信息隧道传送到 HandlerInterceptor
,后者可以将这些标记的请求传递给 real(持久) 防火墙而不牺牲最初标记它们的原始业务逻辑。这里记录的方法应该是相当future-proof,因为它符合HttpFirewall
接口的简单契约,剩下的只是核心Spring框架和JavaServletAPI.
这本质上是 my earlier answer 的更复杂但更完整的替代方法。在这个答案中,我实现了一个新的 StrictHttpFirewall
的子class,它拦截并记录特定日志记录级别的拒绝请求,但也向 HTTP 请求添加了一个属性,为下游过滤器(或控制器)标记它) 处理。此外,此 AnnotatingHttpFirewall
提供了一个 inspect()
方法,允许 subclasses 添加自定义规则来阻止请求。
此解决方案分为两部分:(1) Spring 安全性 和 (2) Spring 框架(核心) ,因为那是首先导致此问题的鸿沟,这说明了如何弥合它。
作为参考,这是在 Spring 4.3.17 和 Spring Security 4.2.6 上测试的。 Spring 5.1 发布时可能会有重大变化。
第 1 部分:Spring 安全性
这是在 Spring 安全性中执行日志记录和标记的解决方案的一半。
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
在 WebSecurityConfig
中,将 HTTP 防火墙设置为 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;
}
}
第 2 部分:Spring 框架
这个解决方案的第二部分可以想象地实现为 ServletFilter
或 HandlerInterceptor
。我要走 HandlerInterceptor
的道路,因为它似乎提供了最大的灵活性,并且可以直接在 Spring 框架内工作。
RequestBlockedException.java
此自定义异常可由错误控制器处理。这可能会扩展到添加任何请求 headers,参数或属性可用的原始请求(甚至是完整的请求本身)可能与应用程序业务逻辑相关(例如,持久防火墙)。
/**
* 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
此拦截器在 Spring 安全过滤器具有 运行 之后调用(即,在 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.java
在 WebConfig
中,将 FirewallInterceptor
添加到注册表。
@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
这专门处理上面的自定义异常,并为客户端生成一个干净的错误页面,同时记录所有相关信息并为自定义应用程序防火墙调用任何特殊业务逻辑。
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
具有抛出 NoHandlerFoundException
的默认映射的控制器。
这绕过了 DispatcherServlet.noHandlerFound 中的 chicken-and-egg 策略,允许该方法 始终 找到一个映射,以便始终调用 FirewallInterceptor.preHandle
。这使 RequestRejectedByFirewallException
优先于 NoHandlerFoundException
。
为什么这是必要的:
如前所述 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()),因此这些请求将进入您的 404 视图(在我的情况下这不是所需的行为 - 您会看到很多 "No mapping found for HTTP request with URI..." 消息)。这可以通过将特殊 header 的检查移动到 noHandlerFound
方法中来解决。不幸的是,如果不从头编写一个新的 Dispatcher Servlet 就没有办法做到这一点,那么你还不如扔掉整个 Spring Framework。由于受保护的、私有的和最终的方法的混合,以及它的属性不可访问的事实(没有 getter 或 setter),不可能扩展 DispatcherServlet
。也不可能把class包裹起来,因为没有可以实现的通用接口。此 class 中的默认映射提供了一种优雅的方式来规避所有这些逻辑。
重要提示:下面的 RequestMapping 将阻止解析静态资源,因为它优先于所有已注册的 ResourceHandler。我仍在为此寻找解决方法,但一种可能是尝试 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;
}
}
结果
当这两个部分都正常工作时,您会看到记录了以下两个警告(第一个在 Spring 安全中,第二个是 Spring 框架(核心)ErrorController
).现在您可以完全控制日志记录和可扩展的应用程序防火墙,您可以根据需要进行调整
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.
也可以通过简单的过滤器处理,会导致404错误响应
@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);
}
}
}
另一种处理方法是使用 Spring AOP. We can create an advice around the FilterChainProxy.doFilter() 方法捕获 HttpFirewall 抛出的任何 RequestRejectedException 并将其转换为 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);
}
}
}
一个非常简单的方法是使用web.xml;在该文件中指定一个错误页面:
<error-page>
<exception-type>org.springframework.security.web.firewall.RequestRejectedException</exception-type>
<location>/request-rejected</location>
</error-page>
对于指定的路径(位置),在@Controller
-注释中添加一个映射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;
}
对于 Spring 安全版本 5.4
及更高版本,您可以简单地创建一个 RequestRejectedHandler
类型的 bean,它将被注入 Spring 安全过滤器链
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();
}
我看到最近 github 在 this commit
中更改了一些可行的解决方案如果您注册 RequestRejectedHandler
类型的 bean,它应该可以工作,或者如我所见,也将通过 WebSecurity
在 WebSecurityConfigurerAdapter
中进行集成。不幸的是,此更改可能未包含在 2.3.3.RELEASE 使用依赖项管理中。它应该出现在 Spring 安全配置 5.4.0-M1 中。依赖管理,版本为2.4.0-M1.
看到这个答案的人迟早会在标准版本中看到这一变化。
我们的是 spring-webmvc (4.3.25.RELEASE) GUI 使用 spring-security-core
(4.2.13.RELEASE) 问题是由于 url 字符串包含 ";jsessionid=D3A0470674704B75756AA10F50AA2CFC"
,分号作为其参数之一。
错误 org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
仅在首次加载网页时发生,导致各种 CSS 格式问题,图像未加载,颜色和字体设置不正确。但是,在刷新同一页面或单击任何导航链接后,下一页可以正常加载并正确实现所有 CSS。此外,此错误 RequestRejectedException
严重污染了日志。
我们想要处理这个问题,以便在创建新会话时,生成的 cookie 使用 jsessionid 处理会话,而不是像第二次以后那样处理查询字符串。
我的解决方案源自 RequestRejectedException
或 CSS 问题,即使对于新会话或首次页面加载也是如此。
更新后的LogAndSuppressRequestRejectedExceptionFilter
如下图
@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;
}
}
}