SpringSecurity的防火墙

723 阅读5分钟

前言

这一章没有干货, 可以不看, 但我不能不讲

说白了, 就是spring security 内部有一个防火墙保护服务器的安全, 该防火墙有两种模式, 严格模式和普通模式

spring security默认是严格模式

知道这些就可以了

HttpFirewall介绍

是什么?

一个Spring Security提供的 HTTP 防火墙

在这个防火墙中 Spring Security 帮我们防住了很多危险请求

HttpFirewall接口便用于处理requestresponse请求的

现在我们分析下这个接口的两个方法

public interface HttpFirewall {
   FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException;
   HttpServletResponse getFirewalledResponse(HttpServletResponse response);
}

getFirewalledRequest这个方法接收了我们的请求, 通过某种方法, 将我们的请求处理成FirewalledRequest对象, 如果失败抛出了一个RequestRejectedException拒绝请求异常

getFirewalledResponse这个方法接受了我们的响应, 将其包装下, 最后返回相同的 HttpServletResponse给我们的客户

这个接口有两个实现类

image-20221128130753240

默认是严格模式的HTTP防火墙

image-20221128134201236

我们还可以定义Spring Bean:

image-20221128134337519

还可以接着WebSecurity的方法配置HttpFirewall:

image-20221128134506179

流程分析

HttpFirewall的执行流程非常简单

HttpFirewall执行流程

HttpFirewall严格模式

上面的流程将整个严格模式HttpFirewall算是研究的差不多了

这里再强调下他有哪些功能

  • rejectForbiddenHttpMethod: 校验请求方法是否合法
  • rejectedBlocklistedUrls: 校验请求中的非法字符
  • rejectedUntrustedHosts: 校验主机信息(这里默认信任所有Host)
  • isNormalized: 判断参数格式是否合法
  • rejectNonPrintableAsciiCharactersInFieldName: 判断请求字符是否合法

HttpFirewall普通模式

普通模式的类名叫DefaultHttpFirewall

在源码中, 首先

FirewalledRequest firewalledRequest = new RequestWrapper(request);

构造了一个 RequestWrapper 对象, 在这个过程中做了

  • 将请求地址中的//格式化为/
  • 将请求中的servletPathpathInfo中的分号隔开的参数提取出来, 只保留路径即可
if (!isNormalized(firewalledRequest.getServletPath()) || !isNormalized(firewalledRequest.getPathInfo())) {

通过isNormalized来判断ServletPathPathInfo是否符合标准

if (containsInvalidUrlEncodedSlash(requestURI)) {

判断requestURI中是否包含编码后的斜杠, 即%2f%2F, 默认是不允许存在编码后的斜杠的, 如果开发者有需求, 则取修改allowUrlEncodedSlash = true即可

默认不建议在项目中使用 DefaultHttpFirewall 方案, 不安全, 如果开发者需要使用, 则默认new一个出来当作Spring Bean就行了

更多信息可以查看: spring-security使用-安全防护HttpFirewall(七) - 意犹未尽 - 博客园 (cnblogs.com)

详细情况

必须是标准化 URL

请求地址必须是标准化 URL。

什么是标准化 URL?标准化 URL 主要从四个方面来判断,我们来看下源码:

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

getRequestURI 就是获取请求协议之外的字符;getContextPath 是获取上下文路径,相当于是 project 的名字;getServletPath 这个就是请求的 servlet 路径,getPathInfo 则是除过 contextPathservletPath 之后剩余的部分。

这四种路径中,都不能包含如下字符串:

"./", "/../" or "/."

限制请求方法

public class StrictHttpFirewall implements HttpFirewall {
    //空的集合
    private static final Set<String> ALLOW_ANY_HTTP_METHOD = Collections.unmodifiableSet(Collections.emptySet());
    private Set<String> allowedHttpMethods = createDefaultAllowedHttpMethods();
    //设置允许的请求方式
    private static Set<String> createDefaultAllowedHttpMethods() {
        Set<String> result = new HashSet();
        result.add(HttpMethod.DELETE.name());
        result.add(HttpMethod.GET.name());
        result.add(HttpMethod.HEAD.name());
        result.add(HttpMethod.OPTIONS.name());
        result.add(HttpMethod.PATCH.name());
        result.add(HttpMethod.POST.name());
        result.add(HttpMethod.PUT.name());
        return result;
    }
    public void setUnsafeAllowAnyHttpMethod(boolean unsafeAllowAnyHttpMethod) {
        //如果是false则设置空的集合
        this.allowedHttpMethods = unsafeAllowAnyHttpMethod ? ALLOW_ANY_HTTP_METHOD : createDefaultAllowedHttpMethods();
    }

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

使用方式

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

请求地址不能有分号

如地址:http://localhost:8080/index;id=ddd

public class StrictHttpFirewall implements HttpFirewall {
    private Set<String> encodedUrlBlacklist = new HashSet();
    private Set<String> decodedUrlBlacklist = new HashSet();

    //;的urlecod和decode的几种
    private static final List<String> FORBIDDEN_SEMICOLON = Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
    public void setAllowSemicolon(boolean allowSemicolon) {
        //如果是false则删除调
        if (allowSemicolon) {
            this.urlBlacklistsRemoveAll(FORBIDDEN_SEMICOLON);
        } else {
            this.urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
        }
    }
    //加入到校验list
    private void urlBlacklistsAddAll(Collection<String> values) {
        this.encodedUrlBlacklist.addAll(values);
        this.decodedUrlBlacklist.addAll(values);
    }
    //清除到校验list
    private void urlBlacklistsRemoveAll(Collection<String> values) {
        this.encodedUrlBlacklist.removeAll(values);
        this.decodedUrlBlacklist.removeAll(values);
    }

    private void rejectedBlacklistedUrls(HttpServletRequest request) {
        Iterator var2 = this.encodedUrlBlacklist.iterator();

        String forbidden;
        do {
            if (!var2.hasNext()) {
                var2 = this.decodedUrlBlacklist.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 + "\"");
    }
}
  • 如果请求URL(无论是URL编码前还是URL编码后)包含了分号(;或者%3b或者%3B)则该请求会被拒绝。

    通过开关函数setAllowSemicolon(boolean) 可以设置是否关闭该规则。缺省使用该规则。

必须是可打印的 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;
}

不能使用双斜杠

@Bean
HttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowUrlEncodedDoubleSlash(true);
    return firewall;
}

% 不被允许

如果需要去掉

@Bean
HttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();    //允许%的配置
    firewall.setAllowUrlEncodedPercent(true);
    return firewall;
}
  • 如果请求URLURL编码后包含了%25(URL编码了的百分号%),或者在URL编码前包含了百分号%则该请求会被拒绝。

    通过开关函数setAllowUrlEncodedPercent(boolean) 可以设置是否关闭该规则。缺省使用该规则。

反斜杠不被允许

如果需要去掉

@Bean
HttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowBackSlash(true);
    firewall.setAllowUrlEncodedSlash(true);
    return firewall;
}
  • 如果请求URL(无论是URL编码前还是URL编码后)包含了斜杠(%2f或者%2F)则该请求会被拒绝。

    通过开关函数setAllowUrlEncodedSlash(boolean) 可以设置是否关闭该规则。缺省使用该规则。

  • 如果请求URL(无论是URL编码前还是URL编码后)包含了反斜杠(\或者%5c或者%5B)则该请求会被拒绝。

    通过开关函数setAllowBackSlash(boolean) 可以设置是否关闭该规则。缺省使用该规则。

. 不被允许

@Bean
HttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowUrlEncodedPeriod(true);
    return firewall;
}
  • 如果请求URLURL编码后包含了URL编码的英文句号.(%2e或者%2E)则该请求会被拒绝。

    通过开关函数setAllowUrlEncodedPeriod(boolean) 可以设置是否关闭该规则。缺省使用该规则。

注意:这里提到的"URL编码后"对应英文是URL encoded,"URL编码前"指的是未执行URL编码的原始URL字符串,或者是"URL编码后"的URL经过解码URL decode得到的URL字符串(应该等于原始URL字符串)。