[SpringSecurity]RequestRejectedHandlerでハンドリングされない件
使用バージョン
+--- org.springframework.boot:spring-boot-starter-security -> 3.3.2
| +--- org.springframework.security:spring-security-config:6.3.1
| | +--- org.springframework.security:spring-security-core:6.3.1
| | | +--- org.springframework.security:spring-security-crypto:6.3.1
| \--- org.springframework.security:spring-security-web:6.3.1
不正なURLパラメータなどをハンドリングしたいときにRequestRejectedHandlerをBean化することがよくある。
@Bean
public RequestRejectedHandler requestRejectedHandler() {
return new HttpStatusRequestRejectedHandler(HttpStatus.BAD_REQUEST.value());
}
しかし、RequestRejectedExceptionがExceptionHandlerの@ExceptionHandler(Exception.class)
でキャッチされてしまうことがあった。
問題のリクエストは以下
curl -X POST -b 'test=てすと' http://localhost:8080/hoge
org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the header: "cookie " has a value "test=ã¦ãã¨" that is not allowed.
以下略
cookieには非ASCII文字は入れてはいけないが、完全クライアント都合なのでこちらでは4xx系を返したい。
中身を見てみると、StrictFirewalledRequest#validateAllowedHeaderValue
の様子
Tomcatがヘッダーを解釈するときにtest=てすと
をtest=ã¦ãã¨
されてしまいこの部分で非制御文字になってしまいエラーになる。
詳細は下の方に記載
SpringSecurityの機能ならFilterで処理されるからExceptionHandlerでキャッチされないのでは?と思うところ
ところがどっこい
Filterを通過して、
org.springframework.web.servlet.DispatcherServle
まで動き、
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod
や、
org.springframework.http.server.ServletServerHttpRequest.getHeaders
あたりまで動いている。
(要するに@ExceptionHandler
でキャッチできる範囲)
対策
@ExceptionHandler(RequestRejectedException.class)
でハンドリングする
単純にExceptionを拾って任意のエラーを返す
@ExceptionHandler(RequestRejectedException.class)
public ResponseEntity<String> handleRequestRejectedException(Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.contentType(new MediaType(MediaType.APPLICATION_PROBLEM_JSON, StandardCharsets.UTF_8))
.body(HttpStatus.BAD_REQUEST.getReasonPhrase());
}
StrictHttpFirewallのsetAllowedHeaderValuesをカスタマイズする
公式に記載がある通り、
ヘッダーをUTF-8変換後にチェックさせる。
HttpServletRequest.getCookies()で取得するときはCookieProcessor経由で取得されているようで、こっちはUTF-8に変換されている。
StrictFirewalledRequest経由で取得されないっぽい
@Bean
public StrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
Pattern allowed = Pattern.compile("\\p{IsAssigned}&&[^\\p{IsControl}]*");
firewall.setAllowedHeaderValues(
(header) -> {
String parsed = new String(header.getBytes(ISO_8859_1), UTF_8);
return allowed.matcher(parsed).matches();
});
return firewall;
}
TomcatでリクエストHeaderが解釈される仕組み
HttpHeaderParser#parseHeaderでヘッダーを読み込んでbyteで保持している
headerData.headerValueの中身はMessageBytesで、
toStringTypeでByteChunkのtoStringを呼び出している。で、ByteChunkのDEFAULT_CHARSETがStandardCharsets.ISO_8859_1なので、UTF-8で解釈されず文字化けする
デフォルトって書いてあるけど変更できなさそう
(コメントにUTF-8にしたいけど仕様だからって書いてますね)
Discussion