HttpFirewall

在测试您定义的模式时,理解其机制以及使用的URL值非常重要。

Servlet规范定义了几个`HttpServletRequest`属性,可以通过getter方法访问,我们可能需要根据这些属性进行匹配。这些属性包括`contextPath`、`servletPath`、`pathInfo`和`queryString`。Spring Security只关注应用内路径的安全性,因此忽略`contextPath`。不幸的是,Servlet规范没有精确定义特定请求URI的`servletPath`和`pathInfo`值的具体内容。例如,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对模式与连接的servletPathpathInfo进行不区分大小写的匹配,忽略queryString

如果您需要更强大的匹配策略,可以使用正则表达式。然后策略实现为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", "")来解决此问题。有关请求改进此问题的 issue,请参阅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方法解决此问题

允许某些用户代理
  • 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()
}