HttpFirewall
了解在针对你定义的模式进行测试时所使用的机制和 URL 值是很重要的。
servlet 规范为 HttpServletRequest
定义了几个可通过 getter 方法访问的属性,我们可能需要与这些属性进行匹配。这些属性包括 contextPath
、servletPath
、pathInfo
和 queryString
。Spring Security 仅对保护应用程序内的路径感兴趣,因此 contextPath
被忽略。不幸的是,servlet 规范并未明确指定对于特定请求 URI,servletPath
和 pathInfo
应包含的确切值。例如,URL 的每个路径段可能包含参数,如 RFC 2396 中所定义(当浏览器不支持 cookie 时,你可能见过这种情况,jsessionid
参数被附加到 URL 中的分号之后。然而,RFC 允许 URL 的任何路径段中存在这些参数。)规范未明确说明这些参数是否应包含在 servletPath
和 pathInfo
值中,并且不同 servlet 容器的行为各不相同。存在一种风险,即当应用程序部署在不会从这些值中剥离路径参数的容器中时,攻击者可以将它们添加到请求的 URL 中,导致模式匹配意外成功或失败。(原始值将在请求离开 FilterChainProxy
后返回,因此仍可供应用程序使用。)传入 URL 还可能存在其他变化。例如,它可能包含路径遍历序列(如 /../
)或多条正斜杠(//
),这也会导致模式匹配失败。某些容器在执行 servlet 映射之前会对其进行规范化,但其他容器则不会。为了防止这些问题,FilterChainProxy
使用 HttpFirewall
策略来检查和包装请求。默认情况下,未规范化的请求会被自动拒绝,并且为了匹配目的,路径参数和重复的斜杠会被移除。(因此,例如,原始请求路径 /secure;hack=1/somefile.html;hack=2
会被返回为 /secure/somefile.html
。)因此,使用 FilterChainProxy
来管理安全过滤器链是至关重要的。请注意,servletPath
和 pathInfo
值由容器解码,因此你的应用程序不应有任何包含分号的有效路径,因为这些部分在匹配时会被移除。
正如前面提到的,默认策略是使用 Ant 风格的路径进行匹配,这可能是大多数用户的最佳选择。该策略在类 AntPathRequestMatcher
中实现,该类使用 Spring 的 AntPathMatcher
对拼接后的 servletPath
和 pathInfo
进行不区分大小写的匹配,忽略 queryString
。
如果你需要一个更强大的匹配策略,你可以使用正则表达式。这时策略实现就是 RegexRequestMatcher
。更多信息请参见 RegexRequestMatcher 的 Javadoc。
在实践中,我们建议你在服务层使用方法安全性来控制对应用程序的访问,而不是完全依赖于在Web应用程序级别定义的安全约束。URL会发生变化,并且很难考虑到应用程序可能支持的所有可能的URL以及请求可能被如何操纵。你应该限制自己只使用一些易于理解的简单Ant路径。始终尝试使用“默认拒绝”的方法,其中你将一个通用通配符(/
or )定义为最后一个以拒绝访问。
在服务层定义的安全性要健壮得多,也更难以绕过,因此您应该始终利用 Spring Security 的方法安全选项。
HttpFirewall
还通过拒绝HTTP响应头中的换行符来防止HTTP响应拆分。
默认情况下,使用 StrictHttpFirewall
实现。此实现会拒绝看似恶意的请求。如果它对您的需求来说过于严格,您可以自定义要拒绝的请求类型。然而,重要的是您要知道这样做可能会使您的应用程序受到攻击。例如,如果您希望使用 Spring MVC 的矩阵变量,可以使用以下配置:
- Java
- XML
- Kotlin
@Bean
public StrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowSemicolon(true);
return firewall;
}
<b:bean id="httpFirewall"
class="org.springframework.security.web.firewall.StrictHttpFirewall"
p:allowSemicolon="true"/>
<http-firewall ref="httpFirewall"/>
@Bean
fun httpFirewall(): StrictHttpFirewall {
val firewall = StrictHttpFirewall()
firewall.setAllowSemicolon(true)
return firewall
}
为了防止跨站追踪 (XST) 和 HTTP 方法篡改,StrictHttpFirewall
提供了一个允许的有效 HTTP 方法列表。默认的有效方法是 DELETE
、GET
、HEAD
、OPTIONS
、PATCH
、POST
和 PUT
。如果您的应用程序需要修改有效的方法,您可以配置一个自定义的 StrictHttpFirewall
bean。以下示例仅允许 HTTP GET
和 POST
方法:
- Java
- XML
- Kotlin
@Bean
public StrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
return firewall;
}
<b:bean id="httpFirewall"
class="org.springframework.security.web.firewall.StrictHttpFirewall"
p:allowedHttpMethods="GET,POST"/>
<http-firewall ref="httpFirewall"/>
@Bean
fun httpFirewall(): StrictHttpFirewall {
val firewall = StrictHttpFirewall()
firewall.setAllowedHttpMethods(listOf("GET", "POST"))
return firewall
}
如果你使用 new MockHttpServletRequest()
,它当前会创建一个空字符串 (""
) 作为 HTTP 方法。这是一个无效的 HTTP 方法,并且会被 Spring Security 拒绝。你可以通过将其替换为 new MockHttpServletRequest("GET", "")
来解决这个问题。有关请求改进此问题的详情,请参阅 SPR_16851。
如果你必须允许任何 HTTP 方法(不推荐),你可以使用 StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true)
。这样做会完全禁用对 HTTP 方法的验证。
StrictHttpFirewall
还会检查头部名称和值以及参数名称。它要求每个字符都有一个定义的代码点并且不是控制字符。
此要求可以根据需要通过以下方法进行放宽或调整:
-
StrictHttpFirewall#setAllowedHeaderNames(Predicate)
-
StrictHttpFirewall#setAllowedHeaderValues(Predicate)
-
StrictHttpFirewall#setAllowedParameterNames(Predicate)
参数值也可以通过 setAllowedParameterValues(Predicate)
进行控制。
例如,要关闭此检查,可以使用始终返回 true
的 Predicate
实例来连接你的 StrictHttpFirewall
:
- Java
- Kotlin
@Bean
public StrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowedHeaderNames((header) -> true);
firewall.setAllowedHeaderValues((header) -> true);
firewall.setAllowedParameterNames((parameter) -> true);
return firewall;
}
@Bean
fun httpFirewall(): StrictHttpFirewall {
val firewall = StrictHttpFirewall()
firewall.setAllowedHeaderNames { true }
firewall.setAllowedHeaderValues { true }
firewall.setAllowedParameterNames { true }
return firewall
}
或者,您可能需要允许特定的值。
例如,iPhone Xʀ 使用的 User-Agent
包含了一个不在 ISO-8859-1 字符集中的字符。由于这个原因,一些应用服务器会将这个值解析成两个单独的字符,后者是一个未定义的字符。
你可以使用 setAllowedHeaderValues
方法来解决这个问题:
- Java
- Kotlin
@Bean
public StrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
Pattern allowed = Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*");
Pattern userAgent = ...;
firewall.setAllowedHeaderValues((header) -> allowed.matcher(header).matches() || userAgent.matcher(header).matches());
return firewall;
}
@Bean
fun httpFirewall(): StrictHttpFirewall {
val firewall = StrictHttpFirewall()
val allowed = Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*")
val userAgent = Pattern.compile(...)
firewall.setAllowedHeaderValues { allowed.matcher(it).matches() || userAgent.matcher(it).matches() }
return firewall
}
在处理头部值时,你可以考虑在验证时将其解析为 UTF-8:
- Java
- Kotlin
firewall.setAllowedHeaderValues((header) -> {
String parsed = new String(header.getBytes(ISO_8859_1), UTF_8);
return allowed.matcher(parsed).matches();
});
firewall.setAllowedHeaderValues {
val parsed = String(header.getBytes(ISO_8859_1), UTF_8)
return allowed.matcher(parsed).matches()
}