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()
}