ServerWebExchangeFirewall
恶意用户可以通过多种方式创建请求来利用应用程序漏洞。Spring Security 提供了 `ServerWebExchangeFirewall` 来允许拒绝看起来像是恶意请求的请求。默认实现是 `StrictServerWebExchangeFirewall`,它会拒绝恶意请求。
例如,请求可能包含路径遍历序列(例如 `/../`)或多个正斜杠(`//`),这些也可能导致模式匹配失败。某些容器在执行 servlet 映射之前会将这些内容标准化,但其他容器不会。为了防止此类问题,`WebFilterChainProxy` 使用 `ServerWebExchangeFirewall` 策略来检查和包装请求。默认情况下,未标准化的请求会被自动拒绝,并且路径参数会被移除以进行匹配。(例如,原始请求路径 `/secure;hack=1/somefile.html;hack=2` 将返回为 `/secure/somefile.html`。)因此,使用 `WebFilterChainProxy` 至关重要。
实际上,我们建议您在服务层使用方法安全来控制对应用程序的访问,而不是完全依赖在 web 应用程序级别定义的安全约束。URL 会发生变化,并且很难考虑到应用程序可能支持的所有可能的 URL 以及请求的操纵方式。您应该限制自己使用一些易于理解的简单模式。始终尝试使用“默认拒绝”方法,其中最后一个定义的通配符(`/` 或 ``)用于拒绝访问。
在服务层定义的安全策略更加健壮,更难以绕过,因此您应该始终利用 Spring Security 的方法安全选项。
您可以通过将其公开为 Bean 来自定义 `ServerWebExchangeFirewall`。
-
Java
-
Kotlin
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
firewall.setAllowSemicolon(true);
return firewall;
}
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
firewall.setAllowSemicolon(true)
return firewall
}
为了防止 跨站点追踪 (XST) 和 HTTP 方法篡改,`StrictServerWebExchangeFirewall` 提供了一个允许的有效 HTTP 方法列表。默认的有效方法为 `DELETE`、`GET`、`HEAD`、`OPTIONS`、`PATCH`、`POST` 和 `PUT`。如果您的应用程序需要修改有效方法,您可以配置一个自定义的 `StrictServerWebExchangeFirewall` bean。以下示例仅允许 HTTP `GET` 和 `POST` 方法。
-
Java
-
Kotlin
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
return firewall;
}
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
firewall.setAllowedHttpMethods(listOf("GET", "POST"))
return firewall
}
如果必须允许任何 HTTP 方法(不推荐),可以使用 `StrictServerWebExchangeFirewall.setUnsafeAllowAnyHttpMethod(true)`。这样做会完全禁用 HTTP 方法的验证。
`StrictServerWebExchangeFirewall` 还检查报头名称和值以及参数名称。它要求每个字符都有一个定义的代码点,而不是控制字符。
可以使用以下方法根据需要放宽或调整此要求。
-
StrictServerWebExchangeFirewall#setAllowedHeaderNames(Predicate)
-
StrictServerWebExchangeFirewall#setAllowedHeaderValues(Predicate)
-
StrictServerWebExchangeFirewall#setAllowedParameterNames(Predicate)
参数值也可以使用 `setAllowedParameterValues(Predicate)` 控制。 |
例如,要关闭此检查,您可以使用始终返回 `true` 的 `Predicate` 实例来连接您的 `StrictServerWebExchangeFirewall`。
-
Java
-
Kotlin
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
firewall.setAllowedHeaderNames((header) -> true);
firewall.setAllowedHeaderValues((header) -> true);
firewall.setAllowedParameterNames((parameter) -> true);
return firewall;
}
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
firewall.setAllowedHeaderNames { true }
firewall.setAllowedHeaderValues { true }
firewall.setAllowedParameterNames { true }
return firewall
}
或者,您可能需要允许特定值。
例如,iPhone Xʀ 使用的 `User-Agent` 包含一个不在 ISO-8859-1 字符集中的字符。由于这个原因,一些应用程序服务器会将此值解析为两个单独的字符,第二个字符是未定义的字符。
您可以使用 `setAllowedHeaderValues` 方法解决此问题。
-
Java
-
Kotlin
@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
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(): StrictServerWebExchangeFirewall {
val firewall = StrictServerWebExchangeFirewall()
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()
}