HttpFirewall

重要的是要理解當針對您定義的模式進行測試時,機制是什麼以及使用了哪個 URL 值。

servlet 規範為 HttpServletRequest 定義了幾個屬性,這些屬性可透過 getter 方法存取,並且我們可能希望針對這些屬性進行匹配。這些屬性是 contextPathservletPathpathInfoqueryString。Spring Security 只對保護應用程式內的路徑感興趣,因此會忽略 contextPath。遺憾的是,servlet 規範並未明確定義特定請求 URI 的 servletPathpathInfo 值所包含的內容。例如,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。有關更多資訊,請參閱 此類別的 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()
}