返回介绍

8.2 HttpFirewall 严格模式

发布于 2025-04-26 13:16:46 字数 9461 浏览 0 评论 0 收藏

HttpFirewall 严格模式就是使用 StrictHttpFirewall,默认即此。本节我们将对严格模式中的规则逐一进行分析。

在 FilterChainProxy#doFilterInternal 中触发请求校验的方法如下:

    private void doFilterInternal(ServletRequest request,
                                       ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
       FirewalledRequest fwRequest = firewall
               .getFirewalledRequest((HttpServletRequest) request);
       HttpServletResponse fwResponse = firewall
               .getFirewalledResponse((HttpServletResponse) response);
       //省略其他
       vfc.doFilter(fwRequest, fwResponse);
    }

可以看到,请求的校验主要是在 getFirewalledRequest 方法中完成的。在进入 Spring Security 过滤器链之前,请求对象和响应对象都分别换成 FirewalledRequest 和 FirewalledResponse 了。如前面所述,FirewalledResponse 主要对响应头参数进行校验,比较简单,这里不再赘述。不过需要注意的是,无论是 FirewalledRequest 还是 FirewalledResponse,在经过 Spring Security 过滤器链的时候,还会通过装饰器模式增强其功能,所以开发者最终在接口中拿到的 HttpServletRequest 和 HttpServletResponse 对象,并不是这里的 FirewalledRequest 和 Firewalled Response。

我们来重点分析 getFirewalledRequest 方法中所做的校验。

StrictHttpFirewall#getFirewalledRequest 源码如下:

    public FirewalledRequest getFirewalledRequest(HttpServletRequest request)
                                                   throws RequestRejectedException {
       rejectForbiddenHttpMethod(request);
       rejectedBlacklistedUrls(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 FirewalledRequest(request) {
           @Override
           public void reset() {
           }
       };
    }

可以看到,在返回对象之前,一共做了五个校验:

(1)rejectForbiddenHttpMethod:校验请求方法是否合法。

(2)rejectedBlacklistedUrls:校验请求中的非法字符。

(3)rejectedUntrustedHosts:检验主机信息。

(4)isNormalized:判断参数格式是否合法。

(5)containsOnlyPrintableAsciiCharacters:判断请求字符是否合法。

下面,我们来逐一分析这五个校验方法。

8.2.1 rejectForbiddenHttpMethod

rejectForbiddenHttpMethod 方法主要用来判断请求方法是否合法:

    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 whitelist " +
                   this.allowedHttpMethods);
       }
    }

allowedHttpMethods 是一个 Set 集合,默认情况下该集合中包含七个常见的方法:DELETE、GET、HEAD、OPTIONS、PATCH、POST、PUT,ALLOW_ANY_HTTP_METHOD 变量默认情况下则是一个空的 Set 集合。根据 rejectForbiddenHttpMethod 方法中的定义,只要你的请求方法是这七个中的任意一个,请求都是可以通过的,不会被拦截。当然开发者也可以根据实际需求修改 allowedHttpMethods 变量的值,进而调整允许的请求方法。

第一种修改方式如下:

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
       @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 集合,集合中保存着允许通过的请求方法,这个集合最终会被赋值给 allowedHttpMethods 变量。配置完成后,重启项目,此时再去访问,就只有 POST 请求可以被处理了,如果发送 GET 请求,那服务端将抛出异常,代码如下:

    org.springframework.security.web.firewall.RequestRejectedException: The
    request was rejected because the HTTP method "GET" was not included within
    the whitelist [POST]

第二种修改方式如下:

    @Bean
    HttpFirewall httpFirewall() {
       StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
       strictHttpFirewall.setUnsafeAllowAnyHttpMethod(true);
       return strictHttpFirewall;
    }

这种方式是直接调用 setUnsafeAllowAnyHttpMethod 方法并设置参数为 true,表示允许所有的请求通过。该方法会设置让 allowedHttpMethods 等于 ALLOW_ANY_HTTP_METHOD,这样会导致在 rejectForbiddenHttpMethod 方法的第一个 if 分支中直接返回,进而达到允许所有请求通过的目的。

8.2.2 rejectedBlacklistedUrls

rejectedBlacklistedUrls 主要用来校验请求 URL 是否规范,对于不规范的请求将会直接拒绝掉。什么样的请求算是不规范的请求呢?

(1)如果请求 URL 地址中在编码之前或者编码之后,包含了分号,即 ; 、%3b、%3B,则该请求会被拒绝。可以通过 setAllowSemicolon 方法开启或者关闭这一规则。

(2)如果请求 URL 地址中在编码之前或者编码之后,包含了斜杠,即%2f、%2F,则该请求会被拒绝。可以通过 setAllowUrlEncodedSlash 方法开启或者关闭这一规则。

(3)如果请求 URL 地址中在编码之前或者编码之后,包含了反斜杠,即\\、%5c、%5C,则该请求会被拒绝。可以通过 setAllowBackSlash 方法开启或者关闭这一规则。

(4)如果请求 URL 在编码之后包含了%25,亦或者在编码之前包含了%,则该请求会被拒绝。可以通过 setAllowUrlEncodedPercent 方法开启或者关闭这一规则。

(5)如果请求 URL 在 URL 编码后包含了英文句号%2e 或者%2E,则该请求会被拒绝。可以通过 setAllowUrlEncodedPeriod 方法开启或者关闭这一规则。

    private void rejectedBlacklistedUrls(HttpServletRequest request) {
       for (String forbidden : this.encodedUrlBlacklist) {
           if (encodedUrlContains(request, forbidden)) {
               throw new RequestRejectedException("The request was rejected
                 because the URL contained a potentially malicious String \""
                                                      + forbidden + "\"");
           }
       }
       for (String forbidden : this.decodedUrlBlacklist) {
           if (decodedUrlContains(request, forbidden)) {
               throw new RequestRejectedException("The request was rejected
                  because the URL contained a potentially malicious String \""
                                                               + forbidden + "\"");
           }
       }
    }

这里一共包含两个 for 循环。第一个校验编码后的请求地址,第二个校验解码后的请求地址。

在 encodedUrlContains 方法中我们可以看到,这里主要是校验了 contextPath 和 requestURI 两个属性,这两个属性是客户端传递来的字符串,未做任何更改。

而在 decodedUrlContains 方法中,主要校验了 servletPath、pathInfo 两个属性,读者可能会觉得这不是重复校验了吗?前面的 requestURI 已经包含所有了!

这里需要注意,requestURI 是客户端发来的请求,是原封不动的,而 servletPath 和 pathInfo 是经过解码的请求地址,所以两者是不一样的。例如客户端发送的请求是 http://localhost:8080/get%3baaa,那么 requestURI 的值就是 http://localhost:8080/get%3baaa,而 servletPath 的值则是/get;aaa(假设 contextPath 为空),即在 servletPath 中,将%3b 还原为分号了。

如果请求地址中含有不规范字符,例如请求 http://localhost:8080/get%3baaa 地址,则控制台报错如下:

    org.springframework.security.web.firewall.RequestRejectedException: The
    request was rejected because the URL contained a potentially malicious String
    "%3b"

8.2.3 rejectedUntrustedHosts

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 将不被信任。配置完成后,重启项目,此时如果访问 http://localhost:8080/get,控制台将会报错,代码如下:

    org.springframework.security.web.firewall.RequestRejectedException:
    The request was rejected because the domain localhost is untrusted.

使用 http://local.javaboy.org:8080/get 地址则可以正常访问。

8.2.4 isNormalized

isNormalized 方法主要用来检查请求地址是否规范,什么样的地址就算规范呢?即不包含"./"、"/../"以及"/."三种字符。

    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;
    }

可以看到,该方法对 requestURI、contextPath、servletPath 以及 pathInfo 分别进行了校验。

如果开发者请求 http://local.javaboy.org:8080/get/../地址,则控制台报错如下:

    org.springframework.security.web.firewall.RequestRejectedException: The
    request was rejected because the URL was not normalized.

8.2.5 containsOnlyPrintableAsciiCharacters

containsOnlyPrintableAsciiCharacters 方法用来校验请求地址中是否包含不可打印的 ASCII 字符:

    private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
       int length = uri.length();
       for (int i = 0; i < length; i++) {
           char c = uri.charAt(i);
           if (c < '\u0020' || c > '\u007e') {
               return false;
           }
       }
       return true;
    }

可打印的 ASCII 字符范围在'\u0020'到'\u007e'之间,对应的十进制就是 32~126 之间,在此范围之外的,属于不可打印的 ASCII 字符。

这就是 StrictHttpFirewall 中的所有校验规则了。其中前三种,开发者可以通过相关方法调整参数进而调整校验行为,后面两种则不可调整。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。