HttpFirewall
理解机制以及在测试所定义模式时使用的URL值至关重要。
Servlet规范为HttpServletRequest定义了若干可通过getter方法访问的属性,这些属性可能需要进行匹配。包括contextPath、servletPath、pathInfo和queryString。Spring Security仅关注应用程序内部路径的安全保护,因此会忽略contextPath。遗憾的是,Servlet规范并未明确定义特定请求URI中servletPath和pathInfo的具体取值。例如,根据RFC 2396的定义,URL的每个路径段都可能包含参数(当浏览器不支持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 风格路径进行匹配,这对大多数用户来说可能是最佳选择。该策略在 PathPatternRequestMatcher 类中实现,它使用 Spring 的 PathPattern 对拼接后的 servletPath 和 pathInfo 执行不区分大小写的模式匹配,并忽略 queryString。
如果您需要更强大的匹配策略,可以使用正则表达式。此时策略实现为 RegexRequestMatcher。更多信息请参阅 RegexRequestMatcher Javadoc。
在实践中,我们建议您在服务层使用方法级安全来控制应用程序的访问权限,而不是完全依赖于在Web应用级别定义的安全约束。URL会发生变化,并且很难考虑到应用程序可能支持的所有可能的URL以及请求可能被操纵的方式。您应该限制自己使用一些易于理解的简单Ant路径。始终尝试采用“默认拒绝”的方法,即最后定义一个通配符(/ 或 /**)来拒绝所有访问。
在服务层定义的安全性更为健壮且难以绕过,因此你应当始终充分利用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()
}