HttpFirewall

理解该机制以及在对照您定义的模式进行测试时使用的 URL 值是什么,这一点很重要。

Servlet 规范定义了 HttpServletRequest 的几个属性,这些属性可以通过 getter 方法访问,我们可能需要对照它们进行匹配。这些属性是 contextPathservletPathpathInfoqueryString。Spring Security 只关注保护应用程序内部的路径,因此 contextPath 会被忽略。不幸的是,Servlet 规范并未精确定义 servletPathpathInfo 对于特定请求 URI 所包含的值。例如,URL 的每个路径段都可以包含参数,如 RFC 2396 中所定义(当浏览器不支持 Cookie 并且 jsessionid 参数被附加到 URL 中分号之后时,您可能见过这种情况。但是,RFC 允许这些参数出现在 URL 的任何路径段中。)规范没有明确说明这些参数是否应包含在 servletPathpathInfo 值中,而且不同 Servlet 容器的行为也各不相同。存在这样一种危险:当应用程序部署在不会从这些值中剥离路径参数的容器中时,攻击者可以将其添加到请求的 URL 中,从而导致模式匹配意外成功或失败。(原始值在请求离开 FilterChainProxy 后会返回,因此仍然可供应用程序使用。)传入 URL 的其他变体也可能发生。例如,它可能包含路径遍历序列(如 /../)或多个正斜杠(//),这也可能导致模式匹配失败。有些容器在执行 Servlet 映射之前会对其进行标准化处理,但有些则不会。为了防范此类问题,FilterChainProxy 使用 HttpFirewall 策略来检查并包装请求。默认情况下,非标准化的请求会自动拒绝,并且为了匹配目的,会移除路径参数和重复的正斜杠。(例如,原始请求路径 /secure;hack=1/somefile.html;hack=2 会被处理为 /secure/somefile.html。)因此,使用 FilterChainProxy 来管理安全过滤器链至关重要。请注意,servletPathpathInfo 值由容器解码,因此您的应用程序不应包含任何包含分号的有效路径,因为这些部分会为了匹配目的而被移除。

如前所述,默认的匹配策略是使用 Ant 风格的路径,这对于大多数用户来说可能是最好的选择。该策略在 AntPathRequestMatcher 类中实现,它使用 Spring 的 AntPathMatcher 对串联的 servletPathpathInfo 进行模式的忽略大小写匹配,并忽略 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 方法列表。默认的有效方法是 DELETEGETHEADOPTIONSPATCHPOSTPUT。如果您的应用程序需要修改有效方法,可以配置自定义的 StrictHttpFirewall bean。以下示例仅允许 HTTP GETPOST 方法:

仅允许 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) 控制。

例如,要关闭此检查,您可以将 StrictHttpFirewall 配置为始终返回 truePredicate 实例:

允许任何请求头名称、请求头值和参数名称
  • 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 方法来解决此问题:

允许某些 User-Agent
  • 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:

将请求头解析为 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()
}