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

posted @ 2021-05-25 01:14  刚刚好。  阅读(2907)  评论(0)    收藏  举报