Spring Security HttpFirewallの仕様確認
Spring Security HttpFirewall を仕様(実装)を確認したのでその内容説明になります。
Spring Security HttpFirewall の説明は以下になります。
https://spring.pleiades.io/spring-security/reference/servlet/exploits/firewall.html
ここでは、DefaultHttpFirewall/StrictHttpFirewall を見ていきます。
名前にDefaultが付いていて紛らわしいですが、SpringSecurityのデフォルト実装はStrictHttpFirewallになります。
[JavaDoc]
DefaultHttpFirewall
https://spring.pleiades.io/spring-security/site/docs/6.2.4/api//org/springframework/security/web/firewall/DefaultHttpFirewall.html#setAllowUrlEncodedSlash(boolean)
StrictHttpFirewall
https://spring.pleiades.io/spring-security/site/docs/6.2.4/api//org/springframework/security/web/firewall/StrictHttpFirewall.html
DefaultHttpFirewall
それではDefaultHttpFirewallから見ていきます。
JavaDocによるとチェック仕様は以下の2点。
[クラス概要]
正規化されていないパス(ディレクトリトラバーサル文字シーケンスを含む)が見つかった場合、リクエストはすぐに拒否されます。
[setAllowUrlEncodedSlashメソッド]
アプリケーションが URL エンコードされたスラッシュ文字を許可するかどうかを設定します。
true(デフォルトは false)の場合、URL エンコードされたスラッシュは URL で許可されます。
注意点としては、
servletPath および pathInfo の一貫した値を提供するためにリクエストをラップするデフォルトの実装であり、パスパラメーター(RFC 2396 で定義されている)を含みません。
です。
それぞれの実装は以下の通りでした。
[正規化されていないパス判定]
/**
* Checks whether a path is normalized (doesn't contain path traversal sequences like
* "./", "/../" or "/.")
* @param path the path to test
* @return true if the path doesn't contain any path-traversal character sequences.
*/
private boolean isNormalized(String path) {
if (path == null) {
return true;
}
for (int i = path.length(); i > 0;) {
int slashIndex = path.lastIndexOf('/', i - 1);
int gap = i - slashIndex;
if (gap == 2 && path.charAt(slashIndex + 1) == '.') {
// ".", "/./" or "/."
return false;
}
if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') {
return false;
}
i = slashIndex;
}
return true;
}
[スラッシュ文字許可判定]
/**
* <p>
* Sets if the application should allow a URL encoded slash character.
* </p>
* <p>
* If true (default is false), a URL encoded slash will be allowed in the URL.
* Allowing encoded slashes can cause security vulnerabilities in some situations
* depending on how the container constructs the HttpServletRequest.
* </p>
* @param allowUrlEncodedSlash the new value (default false)
*/
public void setAllowUrlEncodedSlash(boolean allowUrlEncodedSlash) {
this.allowUrlEncodedSlash = allowUrlEncodedSlash;
}
private boolean containsInvalidUrlEncodedSlash(String uri) {
if (this.allowUrlEncodedSlash || uri == null) {
return false;
}
if (uri.contains("%2f") || uri.contains("%2F")) {
return true;
}
return false;
}
StrictHttpFirewall
次はStrictHttpFirewallになります。
冒頭でも書いたようにこちらのSecuritySecurityのデフォルト実装であり、特に指定しない場合はこちらのFirewallが適用されます。
JavaDocによるとチェック仕様は以下の2点。
[クラス概要]
次のルールがファイアウォールに適用されます。
許可されていない HTTP メソッドを拒否します。これは HTTP 動詞の改ざんと XST 攻撃をブロックするように指定されています。setAllowedHttpMethods(Collection) を参照
セキュリティの制約を回避するために、正規化されていない URL を拒否します。この制約を無効にすることは非常に危険であると考えられるため、これを無効にする方法はありません。この動作を許可するいくつかのオプションは、ファイアウォールの前にリクエストを正規化するか、代わりに DefaultHttpFirewall を使用することです。リクエストの正規化は脆弱であり、リクエストが正規化ではなく拒否される理由に留意してください。
出力可能な ASCII 文字ではない文字を含む URL を拒否します。この制約を無効にすることは非常に危険であると考えられるため、これを無効にする方法はありません。
セミコロンを含む URL を拒否します。setAllowSemicolon(boolean) を参照
URL エンコードされたスラッシュを含む URL を拒否します。setAllowUrlEncodedSlash(boolean) を参照
バックスラッシュを含む URL を拒否します。setAllowBackSlash(boolean) を参照
null 文字を含む URL を拒否します。setAllowNull(boolean) を参照
URL エンコードパーセントを含む URL を拒否します。setAllowUrlEncodedPercent(boolean) を参照
許可されていないホストを拒否します。setAllowedHostnames(Predicate) を参照
許可されていないヘッダー名を拒否します。setAllowedHeaderNames(Predicate) を参照
許可されていないヘッダー値を拒否します。setAllowedHeaderValues(Predicate) を参照
許可されていないパラメーター名を拒否します。setAllowedParameterNames(Predicate) を参照
許可されていないパラメーター値を拒否します。setAllowedParameterValues(Predicate) を参照
基本的に細かい内容は各メソッド参照の形のため、例えば
[setAllowedHeaderNames]メソッドを確認すると
デフォルトでは、ISO 制御文字および定義されていない文字を含むヘッダー名は拒否されます。
となっています。
実装は、以下になります。
private static final Pattern ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN = Pattern
.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*");
private static final Predicate<String> ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE = (
s) -> ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN.matcher(s).matches();
private Predicate<String> allowedHeaderNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
正規表現の部分がキモになっており、正規表現パターン [\p{IsAssigned}&&[^\p{IsControl}]]* は、次のような意味を持ちます:
[\p{IsAssigned}&&[^\p{IsControl}]]* は、Unicodeのすべての割り当てられた文字(\p{IsAssigned})であり、制御文字(\p{IsControl})ではないものを0回以上繰り返すパターンです。
この正規表現パターンは、Unicodeのすべての文字を対象にしており、制御文字を除外しています。制御文字は、改行やタブなどの特定の制御を目的とした文字となります。
例えば、次のような文字列がこの正規表現パターンに一致します:
"Hello, World!" (制御文字が含まれていないため)
"12345" (制御文字が含まれていないため)
"こんにちは" (日本語の文字も一致します)
一方で、次のような文字列は一致しません:
"Hello\tWorld!" (タブ文字が制御文字として含まれているため)
"Line\nBreak" (改行文字が制御文字として含まれているため)