0
0

シングルページアプリケーションやRestAPIにおけるSpring Security でのCSRF対策

Last updated at Posted at 2024-08-17

はじめに

Spring SecurityでCSRF(クロスサイトリクエストフォージェリ)対策を容易に行えます。
しかし、シングルページアプリケーションやRestAPIの場合、Webページに直接トークンを付与することが行えません。
そこで、Spring Securityでは、CookieCsrfTokenRepositoryクラスが用意されており、これを用いて、トークンをCookieに出力し、受け取ったクライアント側アプリがCookieの"XSRF-TOKEN"からトークン値を読み取り、そして読み取ったトークン値をリクエストヘッダ"X-XSRF-TOKEN"に設定してサーバにPOSTすることで、CSRF対策を行えます。

しかし、デフォルトの設定だと、この処理がうまくいかず、Cookieの"XSRF-TOKEN"の値をリクエストヘッダ"X-XSRF-TOKEN"に設定しても、HTTPステータス403エラーとなってしまいます。

これには、かなりハマってしまいました。

原因

デフォルト設定だと、CSRFのトークンチェックにXorCsrfTokenRequestAttributeHandlerが使用されています。
XorCsrfTokenRequestAttributeHandlerでは、トークン値はエンコードされた値をサーバに送信する必要があるのです。
しかし、Cookieに設定されている値はエンコードされている値ではありません。
そのため、Cookieに設定された値をヘッダに付与しても、値が不一致と認識されてしまい、403エラーとなってしまいます。

そのことについては、実は、公式サイトに書いてあります。
https://spring.pleiades.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa

解決策

解決策は2つあります。

解決策1 CsrfTokenRequestAttributeHandler を使用する

上述のXorCsrfTokenRequestAttributeHandler はトークン値をエンコードしているので、それを行わない、CsrfTokenRequestAttributeHandler を使用すれば、とりあえず解決します。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // ...
		http
			// ...
			.csrf((csrf) -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    			.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) // csrfTokenRequestHandlerにCsrfTokenRequestAttributeHandlerを設定
   			);
        // ...
		return http.build();
	}
} 

しかし、CsrfTokenRequestAttributeHandler の場合、トークン値はセッションが有効な間変わりません。リクエストごとにトークン値を変えたい場合は別の方法をとる必要があります。

解決策2 CsrfTokenRequestHandlerを継承してカスタマイズしたHandlerを作成する

これについては、上述の公式サイトにコードがそのまま書いてあります。
https://spring.pleiades.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa

公式サイトより一部引用します。

カスタマイズしたHandoerクラス

final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
	private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
		/*
		 * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
		 * the CsrfToken when it is rendered in the response body.
		 */
		this.delegate.handle(request, response, csrfToken);
	}

	@Override
	public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
		/*
		 * If the request contains a request header, use CsrfTokenRequestAttributeHandler
		 * to resolve the CsrfToken. This applies when a single-page application includes
		 * the header value automatically, which was obtained via a cookie containing the
		 * raw CsrfToken.
		 */
		if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
			return super.resolveCsrfTokenValue(request, csrfToken);
		}
		/*
		 * In all other cases (e.g. if the request contains a request parameter), use
		 * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
		 * when a server-side rendered form includes the _csrf request parameter as a
		 * hidden input.
		 */
		return this.delegate.resolveCsrfTokenValue(request, csrfToken);
	}
}

そして、上述のカスタマイズしたHandlerクラスをSecurityConfigに設定します。

SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())   
				.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // csrfTokenRequestHandlerに作成したカスタムHandlerを設定
			);
		return http.build();
	}
}

これで、リクエストごとにトークン値が変更され、かつ、トークン値がエンコードされなくなるので、403エラーが出なくなります。

最後に

Spring SecurityではCSRF対策を簡単に行える仕組みが用意されているものの、シングルページアプリケーションやRestAPIの場合、デフォルト設定だとうまくいかないという事態が発生します。
実は公式サイトに書いてはいるのですが、備忘録としてここに記述しました。
困っている方の参考になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0