HttpFirewall

了解该机制是什么以及测试针对你定义的模式时所使用的 URL 值非常重要。

Servlet 规范为`HttpServletRequest`定义了多个可通过 getter 方法访问的属性,我们可能希望与之匹配。这些是`contextPath`、servletPathpathInfo`和`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 对 concatenated servletPathpathInfo 执行不区分大小写的模式匹配,并忽略 queryString

如果你需要更强大的匹配策略,可以使用正则表达式。策略实现为`RegexRequestMatcher`。有关更多信息,请参阅该类的{security-api-url}/org/springframework/security/web/util/matcher/RegexRequestMatcher.html[Javadoc]。

实际上,我们建议你在你的服务层使用方法安全性来控制对应用程序的访问,而不是完全依赖于在 Web 应用程序层面定义的安全约束。URL 会发生变化,很难考虑应用程序可能支持的所有可能的 URL 以及请求可能如何被操纵。你应该只使用简单易懂的几个 Ant 路径。始终尝试使用"`deny-by-default`"方法,其中你在最后定义一个通配符(/ or )来拒绝访问。

在服务层定义的安全功能更为强大,也更难绕过,因此你应该始终利用 Spring Security 的方法安全选项。

`HttpFirewall`还通过拒绝 HTTP 响应头中的换行符来防止 HTTP Response Splitting

默认情况下,使用 StrictHttpFirewall 实现。此实现拒绝看似恶意的请求。如果它对你来说过于严格,你可以自定义拒绝哪种类型的请求。但是,这样做时请务必知道,这可能会让你的应用程序面临攻击的风险。例如,如果你希望使用 Spring MVC 的矩阵变量,则可以使用以下配置:

Allow Matrix Variables
  • 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
}

为了防止 Cross Site Tracing (XST)HTTP Verb TamperingStrictHttpFirewall`提供了一个有效的 HTTP 方法允许列表。默认的有效方法是`DELETEGETHEADOPTIONSPATCHPOST`和`PUT。如果你的应用需要修改有效方法,你可以配置一个自定义`StrictHttpFirewall`bean。以下示例仅允许 HTTP `GET`和`POST`方法:

Allow Only 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) 控制参数值。

例如,要关闭此检查,你可以使用始终返回 truePredicate 实例关联你的 StrictHttpFirewall

Allow Any Header Name, Header Value, and Parameter Name
  • 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ʀ 使用包含 ISO-8859-1 字符集内没有的字符的 User-Agent。由于此原因,某些应用程序服务器将此值解析为两个单独的字符,后者是未定义的字符。

你可以通过 setAllowedHeaderValues 方法解决此问题:

Allow Certain User Agents
  • 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:

Parse Headers As 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()
}