ServerWebExchangeFirewall

惡意使用者可以透過各種方式建立請求,進而利用應用程式漏洞。Spring Security 提供了 ServerWebExchangeFirewall 以允許拒絕看起來惡意的請求。預設實作是 StrictServerWebExchangeFirewall,它會拒絕惡意請求。

例如,請求可能包含路徑遍歷序列(例如 `/../`)或多個正斜線(`//`),這些也可能導致模式比對失敗。有些容器會在執行 servlet 映射之前將這些正規化,但其他容器則不會。為了防止這類問題,`WebFilterChainProxy` 使用 ServerWebExchangeFirewall 策略來檢查和包裝請求。預設情況下,未正規化的請求會自動拒絕,並且為了比對目的會移除路徑參數。(因此,例如,原始請求路徑 `/secure;hack=1/somefile.html;hack=2` 會回傳為 `/secure/somefile.html`。)因此,使用 WebFilterChainProxy 至關重要。

實際上,我們建議您在服務層使用方法安全,以控制對應用程式的存取,而不是完全依賴在 Web 應用程式層級定義的安全限制。URL 會變更,而且很難考慮到應用程式可能支援的所有可能 URL 以及請求可能被操控的方式。您應該將自己限制為使用一些簡單易懂的模式。始終嘗試使用「預設拒絕」方法,其中您最後定義一個萬用字元(`/**` 或 `**`)來拒絕存取。

在服務層定義的安全更強大且更難以繞過,因此您應該始終利用 Spring Security 的方法安全選項。

您可以透過將 ServerWebExchangeFirewall 公開為 Bean 來客製化它。

允許矩陣變數
  • Java

  • Kotlin

@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
    StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
    firewall.setAllowSemicolon(true);
    return firewall;
}
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
    val firewall = StrictServerWebExchangeFirewall()
    firewall.setAllowSemicolon(true)
    return firewall
}

為了防止 跨站追蹤 (XST)HTTP 動詞篡改StrictServerWebExchangeFirewall 提供了允許的有效 HTTP 方法清單。預設的有效方法為 DELETEGETHEADOPTIONSPATCHPOSTPUT。如果您的應用程式需要修改有效方法,您可以組態自訂的 StrictServerWebExchangeFirewall Bean。以下範例僅允許 HTTP GETPOST 方法

僅允許 GET 和 POST
  • Java

  • Kotlin

@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
    StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
    firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
    return firewall;
}
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
    val firewall = StrictServerWebExchangeFirewall()
    firewall.setAllowedHttpMethods(listOf("GET", "POST"))
    return firewall
}

如果您必須允許任何 HTTP 方法(不建議),您可以使用 StrictServerWebExchangeFirewall.setUnsafeAllowAnyHttpMethod(true)。這樣做會完全停用 HTTP 方法的驗證。

StrictServerWebExchangeFirewall 也會檢查標頭名稱和值以及參數名稱。它要求每個字元都必須有定義的程式碼點,且不得為控制字元。

這個要求可以根據需要透過使用以下方法來放寬或調整

  • StrictServerWebExchangeFirewall#setAllowedHeaderNames(Predicate)

  • StrictServerWebExchangeFirewall#setAllowedHeaderValues(Predicate)

  • StrictServerWebExchangeFirewall#setAllowedParameterNames(Predicate)

參數值也可以使用 setAllowedParameterValues(Predicate) 來控制。

例如,若要關閉此檢查,您可以將您的 StrictServerWebExchangeFirewall 與始終傳回 truePredicate 實例連接起來

允許任何標頭名稱、標頭值和參數名稱
  • Java

  • Kotlin

@Bean
public StrictServerWebExchangeFirewall httpFirewall() {
    StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
    firewall.setAllowedHeaderNames((header) -> true);
    firewall.setAllowedHeaderValues((header) -> true);
    firewall.setAllowedParameterNames((parameter) -> true);
    return firewall;
}
@Bean
fun httpFirewall(): StrictServerWebExchangeFirewall {
    val firewall = StrictServerWebExchangeFirewall()
    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 StrictServerWebExchangeFirewall httpFirewall() {
    StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
    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(): StrictServerWebExchangeFirewall {
    val firewall = StrictServerWebExchangeFirewall()
    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()
}