HttpFirewall
HttpFirewall 是spring security提供的HTTP防火墙,它可以用于拒绝潜在的危险请求或者包装这些请求进而控制其行为。HttpFirewall 被注入到FilterChainProxy 中,并在spring security过滤器链执行之前被触发。
8.1HttpFirewall 简介
Spring security中通过HttpFirewall 来检查请求路径以及参数是否合法,如果合法,才会进入到过滤器链中进行处理。
public interface HttpFirewall {
FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException;
HttpServletResponse getFirewalledResponse(HttpServletResponse response);
}
FirewalledRequest 是封装后的请求类,但实际上该类只是在HttpServletRequestWrapper 的基础上增加了reset 方法。当spring security过滤器链执行完毕时,由FilterChainProxy 负责调用该方法,以便重置全部或者部分属性。 FirewalledResponse 是封装后的响应类,该类主要重写了sendRedirect 、setHeader 、addHeader 以及addCookie 四个方法,在每一个方法中都对其参数进行校验,以确保参数中不含有\r 和\n 。 HttpFirewall 一共有两个实现类:
DefaultHttpFirewall :虽然名字中包含default,但这并不是框架默认使用的HTTP防火墙,它只是一个检查相对宽松的防火墙。StrictHttpFirewall :这是一个检查严格的HTTP防火墙,默认即此。
HttpFirewall 中对请求的合法性校验在FilterChainProxy#doFilterInternal 方法中触发。 需要注意的是HttpFirewall 的配置位置,在spring security框架中有两个地方涉及了HttpFirewall 实例的获取:
- 在
FilterChainProxy 属性定义中,默认创建的HttpFirewall 实例就是StrictHttpFirewall 。 FilterChainProxy 是在WebSecurity#performBuild 方法中构建的,而WebSecurity 实现了ApplicationContextAware 接口,并实现了接口中的setApplicationContext 方法,在该方法中,从spring容器中查找到HttpFirewall 对并赋值给httpFirewall 属性。最终在performBuild 方法中,将FilterChainProxy 对象构建成功后,如果httpFirewall 不为空,就把httpFirewall 配置给FilterChainProxy 对象。
因此,如果spring容器中存在HttpFirewall 实例,则最终使用spring容器提供的实例;如果不存在,则使用FilterChainProxy 中默认定义的StrictHttpFirewall 。
8.2HttpFirewall 严格模式
在FilterChainProxy#doFilterInternal 中触发请求校验的方法如下:
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(firewallRequest);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}
需要注意的是,无论是FirewalledRequest 还是FirewalledResponse ,在经过spring security过滤器链的时候,还会通过装饰器模式增强其功能,所以开发者最终在接口中拿到的HttpServletRequest 和HttpServletResponse 对象,并不是这里的FirewalledRequest 和FirewalledResponse 。 重点分析getFirewalledRequest 方法:
@Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
rejectForbiddenHttpMethod(request);
rejectedBlocklistedUrls(request);
rejectedUntrustedHosts(request);
if (!isNormalized(request)) {
throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
}
String requestUri = request.getRequestURI();
if (!containsOnlyPrintableAsciiCharacters(requestUri)) {
throw new RequestRejectedException(
"The requestURI was rejected because it can only contain printable ASCII characters.");
}
return new StrictFirewalledRequest(request);
}
接下来会逐一分析这五个校验方法。
8.2.1rejectForbiddenHttpMethod
主要用来判断请求方法是否合法:
private void rejectForbiddenHttpMethod(HttpServletRequest request) {
if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
return;
}
if (!this.allowedHttpMethods.contains(request.getMethod())) {
throw new RequestRejectedException(
"The request was rejected because the HTTP method \"" + request.getMethod()
+ "\" was not included within the list of allowed HTTP methods " + this.allowedHttpMethods);
}
}
allowedHttpMethods 是一个Set 集合,默认情况下该集合中包含七个常见的方法:DELETE 、GET 、HEAD 、OPTIONS 、PATCH 、POST 、PUT ,ALLOW_ANY_HTTP_METHOD 变量默认情况下则是一个空的Set 集合。 开发者可以根据实际需求修改allowedHttpMethods 变量的值,进而调整允许的请求方法。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
HttpFirewall httpFirewall() {
StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
strictHttpFirewall.setUnsafeAllowAnyHttpMethod(true);
return strictHttpFirewall;
}
}
8.2.2rejectedBlocklistedUrls
主要用来校验请求URL是否规范:
- 如果请求URL地址中在编码之前或者之后,包含了分号,则该请求会被拒绝,可以通过
setAllowSemicolon 方法开启或者关闭这一规则。 - 如果请求URL地址中在编码之前或者之后,包含了斜杠,则该请求会被拒绝,可以通过
setAllowUrlEncodedSlash 方法开启或者关闭这一规则。 - 如果请求URL地址中在编码之前或者之后,包含了反斜杠,则该请求会被拒绝,可以通过
setAllowBackSlash 方法开启或者关闭这一规则。 - 如果请求URL地址中在编码之前或者之后,包含了
%25 或者% ,则该请求会被拒绝,可以通过setAllowUrlEncodedPercent 方法开启或者关闭这一规则。 - 如果请求URL地址中在编码之前或者之后,包含了英文句号
%2e 或者%2E ,则该请求会被拒绝,可以通过setAllowUrlEncodedPeriod 方法开启或者关闭这一规则。
private void rejectedBlocklistedUrls(HttpServletRequest request) {
for (String forbidden : this.encodedUrlBlocklist) {
if (encodedUrlContains(request, forbidden)) {
throw new RequestRejectedException(
"The request was rejected because the URL contained a potentially malicious String \""
+ forbidden + "\"");
}
}
for (String forbidden : this.decodedUrlBlocklist) {
if (decodedUrlContains(request, forbidden)) {
throw new RequestRejectedException(
"The request was rejected because the URL contained a potentially malicious String \""
+ forbidden + "\"");
}
}
}
8.2.3rejectedUntrustedHosts
主要用来校验host是否受信任:
private void rejectedUntrustedHosts(HttpServletRequest request) {
String serverName = request.getServerName();
if (serverName != null && !this.allowedHostnames.test(serverName)) {
throw new RequestRejectedException(
"The request was rejected because the domain " + serverName + " is untrusted.");
}
}
allowedHostnames 默认总是返回true ,即默认信任所有的host,可以根据实际需求对此进行配置:
@Bean
HttpFirewall httpFirewall() {
StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
strictHttpFirewall.setAllowedHostnames((hostname) -> hostname.equalsIgnoreCase("local.javaboy.org"));
return strictHttpFirewall;
}
8.2.4isNormalized
主要用来检查请求地址是否规范,即不包含"./"、"/…/“以及”/."三种字符。
private static boolean isNormalized(HttpServletRequest request) {
if (!isNormalized(request.getRequestURI())) {
return false;
}
if (!isNormalized(request.getContextPath())) {
return false;
}
if (!isNormalized(request.getServletPath())) {
return false;
}
if (!isNormalized(request.getPathInfo())) {
return false;
}
return true;
}
8.2.5containsOnlyPrintableAsciiCharacters
用来校验请求地址中是否包含不可打印的ASCII字符。
private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
int length = uri.length();
for (int i = 0; i < length; i++) {
char ch = uri.charAt(i);
if (ch < '\u0020' || ch > '\u007e') {
return false;
}
}
return true;
}
StrictHttpFirewall 中的校验规则,前三种可以通过相关方法调整,后面两种不可调整。
8.3HttpFirewall 普通模式
@Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
FirewalledRequest firewalledRequest = new RequestWrapper(request);
if (!isNormalized(firewalledRequest.getServletPath()) || !isNormalized(firewalledRequest.getPathInfo())) {
throw new RequestRejectedException(
"Un-normalized paths are not supported: " + firewalledRequest.getServletPath()
+ ((firewalledRequest.getPathInfo() != null) ? firewalledRequest.getPathInfo() : ""));
}
String requestURI = firewalledRequest.getRequestURI();
if (containsInvalidUrlEncodedSlash(requestURI)) {
throw new RequestRejectedException("The requestURI cannot contain encoded slash. Got " + requestURI);
}
return firewalledRequest;
}
一般来说,并不建议在项目中使用DefaultHttpFirewall ,如果一定要用,只需要提供一个实例即可:
@Bean
HttpFirewall httpFirewall() {
return new DefaultHttpFirewall();
}
|