SpringSecurity(十六):HttpFirewall
HttpFirewall是Spring Security提供的Http防火墙,它可以用于拒绝潜在的危险请求或者包装这些请求进而控制其行为。通过HttpFirewall可以对各种非法请求提前进行拦截并处理,降低损失。代码层面,HttpFirewall被注入到FilterChainProxy中,并在Spring Security过滤器链执行之前被触发。
HttpFirewall是一个接口,它只有两个方法:
public interface HttpFirewall {
FirewalledRequest getFirewalledRequest(HttpServletRequest var1) throws RequestRejectedException;
HttpServletResponse getFirewalledResponse(HttpServletResponse var1);
}
getFirewalledRequest方法对请求对象进行检验并封装,getFirewalledResponse方法则对响应对象进行封装。
FirewalledRequest是封装后的请求类,但实际上该类只是在HttpServletRequestWrapper的基础上增加了reset方法,当Spring Security过滤器链执行完毕后,由FilterChainProxy负责调用rest方法,以便重置全部或部分属性。
FirwalledResponse是封装后的相应类,该类主要重写了sendRedirect,setHeader,addHeader以及addCookie四个方法,在每一个方法中都对其参数进行校验,以确保参数不含有\r和\n。
HttpFirewall一共有两个实现类。
DefaultHttpFirewall:虽然名字包含Default,但这并不是框架默认的Http防火墙,它只是一个检查相对宽松的防火墙。
StrictHttpFirewall:这是一个检查严格的Http防火墙,也是框架默认使用的Http防火墙。
如果开发者不想使用默认的StrictHttpFirewall,只需要自己向容器中注册一个HttpFirewall实例即可
StrictHttpFirewall
我们先看下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 = this.getFilters((HttpServletRequest)firewallRequest);
if (filters != null && filters.size() != 0) {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> {
return "Securing " + requestLine(firewallRequest);
}));
}
FilterChainProxy.VirtualFilterChain virtualFilterChain = new FilterChainProxy.VirtualFilterChain(firewallRequest, chain, filters);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
} else {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> {
return "No security for " + requestLine(firewallRequest);
}));
}
firewallRequest.reset();
chain.doFilter(firewallRequest, firewallResponse);
}
}
可以看到,请求的校验主要是在getFirewalledRequest方法中完成的,在进入Spring Security过滤器链之前,请求对象和响应对象都分别替换成FirewalledRequest,FirewalledResponse。如前面所述,FirewalledResponse主要对响应头参数进行校验,比较简单,这里就不在赘述。不过需要注意的是,FirewalledRequest,FirewalledResponse在经过Spring Security过滤器链的时候还会通过装饰器模式增强其功能,所以我们最终在接口中拿到的是HttpServlet和HttpServletResponse对象,并不是这里的FirewalledRequest和FirewalledResponse。
我们重点分析一下getFirewalledRequest方法
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
this.rejectForbiddenHttpMethod(request);
this.rejectedBlocklistedUrls(request);
this.rejectedUntrustedHosts(request);
if (!isNormalized(request)) {
throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
} else {
String requestUri = request.getRequestURI();
if (!containsOnlyPrintableAsciiCharacters(requestUri)) {
throw new RequestRejectedException("The requestURI was rejected because it can only contain printable ASCII characters.");
} else {
return new StrictHttpFirewall.StrictFirewalledRequest(request);
}
}
}
可以看到一共做了五个校验:
(1)rejectForbiddenHttpMethod:校验请求方法是否合法
private void rejectForbiddenHttpMethod(HttpServletRequest request) {
if (this.allowedHttpMethods != ALLOW_ANY_HTTP_METHOD) {
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集合存储了get,post,head,options,patch,put,delete这七个方法。ALLOW_ANY_HTTP_METHOD默认是一个空集合。rejectForbiddenHttpMethod表明只要你的请求不是这七个方法,就会被拦截。当然我们可以根据实际需求修改allowedHttpMethods集合的值
@Bean
HttpFirewall httpFirewall(){
StrictHttpFirewall strictHttpFirewall=new StrictHttpFirewall();
Set<String> allowedHttpMethods = new HashSet<>();
allowedHttpMethods.add(HttpMethod.POST.name());
strictHttpFirewall.setAllowedHttpMethods(allowedHttpMethods);
return strictHttpFirewall;
}
有我们自已提供一个HttpFirewall注入容器,并通过setAllowedHttpMethods传入一个set集合。可以看到此时我们的实例,只允许post方法进入过滤器链。
(2)rejectedBlocklistedUrls:用来校验请求URL是否规范,对于不规范的请求直接拒绝。
什么样的请求会被拒绝?
如果请求URL(无论是URL编码前还是URL编码后)包含了分号(;或者%3b或者%3B)则该请求会被拒绝。通过开关函数setAllowSemicolon(boolean) 可以设置是否关闭该规则。缺省使用该规则。
如果请求URL(无论是URL编码前还是URL编码后)包含了斜杠(%2f或者%2F)则该请求会被拒绝。通过开关函数setAllowUrlEncodedSlash(boolean) 可以设置是否关闭该规则。缺省使用该规则。
如果请求URL(无论是URL编码前还是URL编码后)包含了反斜杠(\或者%5c或者%5B)则该请求会被拒绝。通过开关函数setAllowBackSlash(boolean) 可以设置是否关闭该规则。缺省使用该规则。
如果请求URL在URL编码后包含了%25(URL编码了的百分号%),或者在URL编码前包含了百分号%则该请求会被拒绝。通过开关函数setAllowUrlEncodedPercent(boolean) 可以设置是否关闭该规则。缺省使用该规则。
如果请求URL在URL编码后包含了URL编码的英文句号.(%2e或者%2E)则该请求会被拒绝。通过开关函数setAllowUrlEncodedPeriod(boolean) 可以设置是否关闭该规则。缺省使用该规则。
private void rejectedBlocklistedUrls(HttpServletRequest request) {
Iterator var2 = this.encodedUrlBlocklist.iterator();
String forbidden;
do {
if (!var2.hasNext()) {
var2 = this.decodedUrlBlocklist.iterator();
do {
if (!var2.hasNext()) {
return;
}
forbidden = (String)var2.next();
} while(!decodedUrlContains(request, forbidden));
throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
}
forbidden = (String)var2.next();
} while(!encodedUrlContains(request, forbidden));
throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
}
这里一共有两层while循环,一层校验编码后的请求地址,一层校验解码后的请求地址。
在encodedUrlContains方法中检验contextPath和requstURI两个属性,这两个属性是客户端传递的字符串,未做任何更改。
在ecodedUrlContains方法中校验servletPath,pathInfo两个属性,这两个属性是经过解码后的请求地址。例如客服端发来的请求的http://localhost:8085/get%3baaa,那么requstURI就是http://localhost:8085/get%3baaa,而servletPath的值是/get;aaa(假设contextPath为空),即在servletPath把%3b还原为分号了
(3)rejectedUntrustedHosts:主要校验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.");
}
}
可以看到主要是对serverName的校验,allowedHostnames默认总是返回true,即默认信任所有的Host,开发者可以根据实际需求对此进行配置
@Bean
HttpFirewall httpFirewall(){
StrictHttpFirewall strictHttpFirewall=new StrictHttpFirewall();
strictHttpFirewall.setAllowedHostnames(hostname -> hostname.equalsIgnoreCase("local.javaboy.org"));
return strictHttpFirewall;
}
上述配置表名host必须是local.javaboy.org,其他host不会被信任。
(4)isNormalized:检查请求地址是否规范,即不包含"./" "/../" 以及“/.”三种字符
private static boolean isNormalized(HttpServletRequest request) {
if (!isNormalized(request.getRequestURI())) {
return false;
} else if (!isNormalized(request.getContextPath())) {
return false;
} else if (!isNormalized(request.getServletPath())) {
return false;
} else {
return isNormalized(request.getPathInfo());
}
}
(5)containsOnlyPrintableAsciiCharacters:检查请求地址中是否包含不可打印的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 < ' ' || ch > '~') {
return false;
}
}
return true;
}
DefaultHttpFiredwall
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
FirewalledRequest firewalledRequest = new RequestWrapper(request);
if (this.isNormalized(firewalledRequest.getServletPath()) && this.isNormalized(firewalledRequest.getPathInfo())) {
String requestURI = firewalledRequest.getRequestURI();
if (this.containsInvalidUrlEncodedSlash(requestURI)) {
throw new RequestRejectedException("The requestURI cannot contain encoded slash. Got " + requestURI);
} else {
return firewalledRequest;
}
} else {
throw new RequestRejectedException("Un-normalized paths are not supported: " + firewalledRequest.getServletPath() + (firewalledRequest.getPathInfo() != null ? firewalledRequest.getPathInfo() : ""));
}
}
这里进行了一个isNormalized(同上面的一样)和一个containsInvalidUrlEncodedSlash判断(requestURI是否包含编码后的斜杠,即%2f或%2F)
一般来时不建议开发者使用DefaultHttpFiredwall

浙公网安备 33010602011771号