🕌

Spring Securityが投げるRequestRejectedExceptionをハンドリングする

2023/09/08に公開

やりたいこと

Spring Security 5.7.10を使用しています。

Spring Securityはデフォルトで StrictHttpFirewall を使用して、下記に挙げるような不正っぽいリクエストを拒否してくれます。

  • 許可していないHTTPメソッドを使用したリクエスト
  • 正規化されていないURL
  • URLにセミコロンを含む
ログ
2023-09-08 12:19:15.611 ERROR 9276 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
	at org.springframework.security.web.firewall.StrictHttpFirewall.rejectedBlocklistedUrls(StrictHttpFirewall.java:535)
	at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:505)
	// 省略

拒否した場合は、RequestRejectedException を投げて500エラーとなります。
バグなどに気づきやすいようにエラーをSlack等に通知している場合、この種のエラーはノイズになりがちです。
例外をハンドリングしてINFOレベルでログに出力できないか試しました。

やってみる

Controllerに到達する前にエラーとなるので、@ExceptionHandler を使用して例外ハンドラーを定義しても機能しません。
Spring Securityが用意している RequestRejectedHandler を実装して、Beanを登録することで、例外をハンドリングできます。

Spring Securityが用意している HttpStatusRequestRejectedHandler を参考に CustomHttpStatusRequestRejectedHandler を作成します。
httpError でステータスコードを、handle() で例外発生時の処理を定義しています。

RequestRejectedHandler.java
public class CustomHttpStatusRequestRejectedHandler implements RequestRejectedHandler {

    private static final Log logger = LogFactory.getLog(CustomHttpStatusRequestRejectedHandler.class);

    private final int httpError;

    /**
     * Constructs an instance which uses {@code 400} as response code.
     */
    public CustomHttpStatusRequestRejectedHandler() {
        this.httpError = HttpServletResponse.SC_BAD_REQUEST;
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       RequestRejectedException requestRejectedException) throws IOException {
        logger.info(LogMessage.format("Rejecting request due to: %s", requestRejectedException.getMessage()),
                requestRejectedException); // ログレベルはINFO
        response.sendError(this.httpError); // 400エラーとしてレスポンスする
    }
}

続いて、作成したハンドラーをBean登録します。

@Bean
public RequestRejectedHandler requestRejectedHandler() {
    return new CustomHttpStatusRequestRejectedHandler();
}

やることはこれだけです。
試しに http://localhost:8080/; にリクエストを投げると下記のようにINFOレベルでログが出力されました🙌

ログ
2023-09-08 11:48:16.825  INFO 89178 --- [nio-8080-exec-1] w.CustomHttpStatusRequestRejectedHandler : Rejecting request due to: The request was rejected because the URL contained a potentially malicious String ";"

org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
	at org.springframework.security.web.firewall.StrictHttpFirewall.rejectedBlocklistedUrls(StrictHttpFirewall.java:535)
	at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:505)
	// 省略
BABYJOB テックブログ

Discussion