HttpFirewall
理解该机制以及在对照您定义的模式进行测试时使用的 URL 值是什么,这一点很重要。
Servlet 规范定义了 HttpServletRequest
的几个属性,这些属性可以通过 getter 方法访问,我们可能需要对照它们进行匹配。这些属性是 contextPath
、servletPath
、pathInfo
和 queryString
。Spring Security 只关注保护应用程序内部的路径,因此 contextPath
会被忽略。不幸的是,Servlet 规范并未精确定义 servletPath
和 pathInfo
对于特定请求 URI 所包含的值。例如,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 路径。始终尝试使用“默认拒绝”方法,即最后定义一个全匹配的通配符(/
或 )来拒绝访问。
在服务层定义的安全性更加健壮,也更难绕过,因此您应该始终利用 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
}
如果您使用 |
如果您必须允许任何 HTTP 方法(不推荐),可以使用 StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true)
。这样做会完全禁用对 HTTP 方法的验证。
StrictHttpFirewall
还会检查请求头名称、值和参数名称。它要求每个字符都有定义的码点,且不是控制字符。
可以使用以下方法根据需要放宽或调整此要求:
-
StrictHttpFirewall#setAllowedHeaderNames(Predicate)
-
StrictHttpFirewall#setAllowedHeaderValues(Predicate)
-
StrictHttpFirewall#setAllowedParameterNames(Predicate)
参数值也可以通过 |
例如,要关闭此检查,您可以将 StrictHttpFirewall
配置为始终返回 true
的 Predicate
实例:
-
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()
}