WebFlux 環境向けのクロスサイトリクエストフォージェリ(CSRF)

このセクションでは、WebFlux 環境に対する Spring Security のクロスサイトリクエストフォージェリ (CSRF) サポートについて説明します。

Spring Security CSRF 保護の使用

Spring Security の CSRF 保護を使用する手順の概要は次のとおりです。

適切な HTTP 動詞を使用する

CSRF 攻撃から保護するための最初のステップは、Web サイトが適切な HTTP 動詞を使用するようにすることです。これについては、安全なメソッドは読み取り専用である必要があるで詳しく説明しています。

CSRF 保護を構成する

次のステップは、アプリケーション内で Spring Security の CSRF 保護を構成することです。デフォルトでは、Spring Security の CSRF 保護が有効になっていますが、構成をカスタマイズする必要がある場合があります。次のいくつかのサブセクションでは、いくつかの一般的なカスタマイズについて説明します。

カスタム CsrfTokenRepository

デフォルトでは、Spring Security は、WebSessionServerCsrfTokenRepository を使用して、予想される CSRF トークンを WebSession に格納します。場合によっては、カスタム ServerCsrfTokenRepository を構成する必要があります。例: JavaScript ベースのアプリケーションをサポートするために、CsrfToken を Cookie に永続化することができます。

デフォルトでは、CookieServerCsrfTokenRepository は XSRF-TOKEN という名前の Cookie に書き込み、X-XSRF-TOKEN という名前のヘッダーまたは HTTP _csrf パラメーターからその Cookie を読み取ります。これらのデフォルトは AngularJS (英語) から来ています

Java 構成で CookieServerCsrfTokenRepository を構成できます。

CSRF トークンを Cookie に保存する
  • Java

  • Kotlin

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	http
		// ...
		.csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
	return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        csrf {
            csrfTokenRepository = CookieServerCsrfTokenRepository.withHttpOnlyFalse()
        }
    }
}

上記のサンプルでは、cookieHttpOnly=false を明示的に設定しています。これは、JavaScript(この場合は AngularJS)に読み取らせるために必要です。JavaScript で Cookie を直接読み取る機能が必要ない場合は、セキュリティを向上させるために cookieHttpOnly=false を省略することをお勧めします(代わりに new CookieServerCsrfTokenRepository() を使用します)。

CSRF 保護を無効にする

デフォルトでは、CSRF 保護は有効になっています。ただし、アプリケーションにとって意味がある場合は、CSRF 保護を無効にすることができます。

以下の Java 構成は、CSRF 保護を無効にします。

CSRF 構成を無効にする
  • Java

  • Kotlin

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	http
		// ...
		.csrf(csrf -> csrf.disable()))
	return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        csrf {
            disable()
        }
    }
}

ServerCsrfTokenRequestHandler の設定

Spring Security の CsrfWebFilter (Javadoc) は、ServerCsrfTokenRequestHandler (Javadoc) の助けを借りて、org.springframework.security.web.server.csrf.CsrfToken という名前の ServerWebExchange 属性として Mono<CsrfToken> (Javadoc) を公開します。5.8 では、デフォルトの実装は ServerCsrfTokenRequestAttributeHandler で、交換属性として Mono<CsrfToken> を使用できるようにするだけです。

6.0 の時点で、デフォルトの実装は XorServerCsrfTokenRequestAttributeHandler であり、BREACH に対する保護を提供します ( gh-4001 [GitHub] (英語) を参照)。

CsrfToken の BREACH 保護を無効にして 5.8 のデフォルトに戻す場合は、次の Java 構成を使用して ServerCsrfTokenRequestAttributeHandler を構成できます。

ブリーチ保護を無効にする
  • Java

  • Kotlin

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	http
		// ...
		.csrf(csrf -> csrf
			.csrfTokenRequestHandler(new ServerCsrfTokenRequestAttributeHandler())
		)
	return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        csrf {
            csrfTokenRequestHandler = ServerCsrfTokenRequestAttributeHandler()
        }
    }
}

CSRF トークンを含める

シンクロナイザートークンパターンが CSRF 攻撃から保護するには、実際の CSRF トークンを HTTP リクエストに含める必要があります。ブラウザーによって HTTP リクエストに自動的に含まれないリクエストの一部(フォームパラメーター、HTTP ヘッダー、その他のオプション)に含める必要があります。

Mono<CsrfToken> が ServerWebExchange 属性として公開されている見た。これは、任意のビューテクノロジーが Mono<CsrfToken> にアクセスして、予期されるトークンをフォームまたはメタタグとして公開できることを意味します。

ビューテクノロジーが Mono<CsrfToken> をサブスクライブする簡単な方法を提供しない場合、一般的なパターンは Spring の @ControllerAdvice を使用して CsrfToken を直接公開することです。次の例では、CsrfToken を Spring Security の CsrfRequestDataValueProcessor で使用されるデフォルトの属性名(_csrf)に配置して、CSRF トークンを非表示の入力として自動的に含めます。

 @ModelAttribute としての CsrfToken 
  • Java

  • Kotlin

@ControllerAdvice
public class SecurityControllerAdvice {
	@ModelAttribute
	Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
		Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());
		return csrfToken.doOnSuccess(token -> exchange.getAttributes()
				.put(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, token));
	}
}
@ControllerAdvice
class SecurityControllerAdvice {
    @ModelAttribute
    fun csrfToken(exchange: ServerWebExchange): Mono<CsrfToken> {
        val csrfToken: Mono<CsrfToken>? = exchange.getAttribute(CsrfToken::class.java.name)
        return csrfToken!!.doOnSuccess { token ->
            exchange.attributes[CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME] = token
        }
    }
}

幸いなことに、Thymeleaf は追加作業なしで機能する統合を提供します。

エンコードされたフォーム URL

HTML フォームを投稿するには、CSRF トークンを非表示の入力としてフォームに含める必要があります。次の例は、レンダリングされた HTML がどのように見えるかを示しています。

CSRF トークン HTML
<input type="hidden"
	name="_csrf"
	value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>

次に、CSRF トークンを非表示の入力としてフォームに含めるさまざまな方法について説明します。

自動 CSRF トークンの包含

Spring Security の CSRF サポートは、CsrfRequestDataValueProcessor (Javadoc) を介して Spring の RequestDataValueProcessor (Javadoc) との統合を提供します。CsrfRequestDataValueProcessor が機能するには、Mono<CsrfToken> をサブスクライブし、CsrfToken を DEFAULT_CSRF_ATTR_NAME (Javadoc) と一致する属性として公開する必要があります。

幸い、Thymeleaf は RequestDataValueProcessor と統合することですべての定型文 (英語) を処理し、安全でない HTTP メソッド(POST)を持つフォームに実際の CSRF トークンが自動的に含まれるようにします。

CsrfToken リクエスト属性

リクエストに実際の CSRF トークンを含めるための他のオプションが機能しない場合、Mono<CsrfToken> が org.springframework.security.web.server.csrf.CsrfToken という名前の ServerWebExchange 属性として公開されているという事実を利用できます。

次の Thymeleaf サンプルは、_csrf という名前の属性で CsrfToken を公開することを前提としています。

リクエスト属性を持つフォームの CSRF トークン
<form th:action="@{/logout}"
	method="post">
<input type="submit"
	value="Log out" />
<input type="hidden"
	th:name="${_csrf.parameterName}"
	th:value="${_csrf.token}"/>
</form>

Ajax および JSON リクエスト

JSON を使用する場合、HTTP パラメーター内で CSRF トークンを送信することはできません。代わりに、HTTP ヘッダー内でトークンを送信できます。

次のセクションでは、JavaScript ベースのアプリケーションで HTTP リクエストヘッダーとして CSRF トークンを含めるさまざまな方法について説明します。

自動包含

予想される CSRF トークンを Cookie に保存するように Spring Security を構成できます。予期される CSRF を Cookie に保存することにより、AngularJS (英語) などの JavaScript フレームワークは、実際の CSRF トークンを HTTP リクエストヘッダーに自動的に含めます。

メタタグ

Cookie で CSRF を公開する別のパターンは、meta タグ内に CSRF トークンを含めることです。HTML は次のようになります。

CSRF メタタグ HTML
<html>
<head>
	<meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
	<meta name="_csrf_header" content="X-CSRF-TOKEN"/>
	<!-- ... -->
</head>
<!-- ... -->

メタタグに CSRF トークンが含まれると、JavaScript コードはメタタグを読み取り、CSRF トークンをヘッダーとして含めることができます。jQuery を使用する場合は、次のコードを使用してメタタグを読み取ることができます。

AJAX 送信 CSRF トークン
$(function () {
	var token = $("meta[name='_csrf']").attr("content");
	var header = $("meta[name='_csrf_header']").attr("content");
	$(document).ajaxSend(function(e, xhr, options) {
		xhr.setRequestHeader(header, token);
	});
});

次のサンプルは、_csrf という名前の属性で CsrfToken を公開することを前提としています。次の例では、Thymeleaf を使用してこれを実行します。

CSRF メタタグ JSP
<html>
<head>
	<meta name="_csrf" th:content="${_csrf.token}"/>
	<!-- default header name is X-CSRF-TOKEN -->
	<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
	<!-- ... -->
</head>
<!-- ... -->

CSRF の考慮事項

CSRF 攻撃に対する保護を実装する際に、考慮すべき特別な考慮事項がいくつかあります。このセクションでは、WebFlux 環境に関連するこれらの考慮事項について説明します。より一般的な説明については、CSRF の考慮事項を参照してください。

ログイン

偽造されたログイン試行から保護するために、ログインリクエストには CSRF をリクエストする必要があります。Spring Security の WebFlux サポートはこれを自動的に行います。

ログアウト

ログアウトの試行を偽造しないように保護するために、ログアウトリクエストには CSRF をリクエストする必要があります。デフォルトでは、Spring Security の LogoutWebFilter は HTTP POST リクエストのみを処理します。これにより、ログアウトに CSRF トークンが必要になり、悪意のあるユーザーがユーザーを強制的にログアウトできないようになります。

最も簡単な方法は、フォームを使用してログアウトすることです。本当にリンクが必要な場合は、JavaScript を使用して、リンクに POST を実行させることができます(おそらく非表示のフォームで)。JavaScript が無効になっているブラウザーの場合、オプションで、POST を実行するログアウト確認ページにユーザーを移動させるリンクを設定できます。

ログアウトで HTTP GET を本当に使用したい場合は、そうすることができますが、一般的には推奨されないことに注意してください。例: 次の Java 構成は、任意の HTTP メソッドで /logout URL がリクエストされたときにログアウトします。

HTTP GET でログアウトする
  • Java

  • Kotlin

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	http
		// ...
		.logout(logout -> logout.requiresLogout(new PathPatternParserServerWebExchangeMatcher("/logout")))
	return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        logout {
            requiresLogout = PathPatternParserServerWebExchangeMatcher("/logout")
        }
    }
}

CSRF およびセッションタイムアウト

デフォルトでは、Spring Security は CSRF トークンを WebSession に保存します。この取り決めにより、セッションが期限切れになる状況が発生する可能性があります。これは、検証する CSRF トークンが予期されていないことを意味します。

セッションタイムアウトの一般的な解決策についてはすでに説明しました。このセクションでは、WebFlux サポートに関連する CSRF タイムアウトの詳細について説明します。

予想される CSRF トークンのストレージを変更して Cookie に含めることができます。詳細については、カスタム CsrfTokenRepository のセクションを参照してください。

マルチパート (ファイルアップロード)

マルチパートリクエスト(ファイルのアップロード)を CSRF 攻撃から保護すると、鶏が先か卵が先か [Wikipedia] という問題がどのように発生するかについては、すでに説明しました。このセクションでは、CSRF トークンを WebFlux アプリケーション内の本文URL に配置する方法について説明します。

Spring でのマルチパートフォームの使用の詳細については、Spring リファレンスのマルチパートデータセクションを参照してください。

CSRF トークンを本文に配置する

CSRF トークンを本体に配置することのトレードオフについてはすでに説明しました。

WebFlux アプリケーションでは、次の構成でこれを行うことができます。

multipart/form-data から CSRF トークンの取得を有効にします
  • Java

  • Kotlin

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	http
		// ...
		.csrf(csrf -> csrf.tokenFromMultipartDataEnabled(true))
	return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
		// ...
        csrf {
            tokenFromMultipartDataEnabled = true
        }
    }
}

URL に CSRF トークンを含める

CSRF トークンを URL に配置することのトレードオフについてはすでに説明しました。 CsrfToken は ServerHttpRequest リクエスト属性として公開されているため、これを使用して、CSRF トークンを含む action を作成できます。Thymeleaf の例を以下に示します。

実行中の CSRF トークン
<form method="post"
	th:action="@{/upload(${_csrf.parameterName}=${_csrf.token})}"
	enctype="multipart/form-data">

HiddenHttpMethodFilter

HTTP メソッドのオーバーライドについてはすでに説明しました。

Spring WebFlux アプリケーションでは、HTTP メソッドのオーバーライドは HiddenHttpMethodFilter (Javadoc) を使用して行われます。