クロスサイトリクエストフォージェリ (CSRF)

エンドユーザーがログインできるアプリケーションでは、クロスサイトリクエストフォージェリ (CSRF) から保護する方法を検討することが重要です。

Spring Security は、デフォルトで POST リクエストなどの安全でない HTTP メソッドに対する CSRF 攻撃から保護するため、追加のコードは必要ありません。以下を使用して、デフォルト構成を明示的に指定できます。

CSRF 保護を構成する
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf(Customizer.withDefaults());
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf { }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf/>
</http>

アプリケーションの CSRF 保護の詳細については、次の使用例を検討してください。

CSRF 保護のコンポーネントを理解する

CSRF 保護は、CsrfFilter (Javadoc) 内で構成されるいくつかのコンポーネントによって提供されます。

csrf
図 1: CsrfFilter のコンポーネント

CSRF 保護は 2 つの部分に分かれています。

  1. CsrfTokenRequestHandler に委譲することで、アプリケーションで CsrfToken (Javadoc) を使用できるようにします。

  2. リクエストに CSRF 保護が必要かどうかを判断し、トークンをロードして検証し、AccessDeniedException を処理します

csrf processing
図 2: CsrfFilter 処理
  • number 1 まず、DeferredCsrfToken (Javadoc) がロードされます。これは、永続化された CsrfToken を後でロードできるように、CsrfTokenRepository への参照を保持します (number 4)。

  • number 2 次に、Supplier<CsrfToken> ( DeferredCsrfToken から作成) が CsrfTokenRequestHandler に与えられます。CsrfTokenRequestHandler は、アプリケーションの残りの部分で CsrfToken を使用できるようにするためのリクエスト属性を設定します。

  • number 3 次に、メインの CSRF 保護処理が開始され、現在のリクエストに CSRF 保護が必要かどうかがチェックされます。必要がなければ、フィルターチェーンを継続して処理を終了します。

  • number 4CSRF 保護が必要な場合は、永続化された CsrfToken が最終的に DeferredCsrfToken からロードされます。

  • number 5 続いて、クライアントによって提供された実際の CSRF トークン (存在する場合) は、CsrfTokenRequestHandler を使用して解決されます。

  • number 6 実際の CSRF トークンは、永続化された CsrfToken と比較されます。有効な場合、フィルターチェーン は継続され、処理は終了します。

  • number 7 実際の CSRF トークンが無効 (または欠落) の場合、AccessDeniedException が AccessDeniedHandler に渡され、処理は終了します。

Spring Security 6 への移行

Spring Security 5 から 6 に移行する場合、アプリケーションに影響を与える可能性のある変更がいくつかあります。以下は、Spring Security 6 で変更された CSRF 保護の側面の概要です。

Spring Security 6 の変更にはシングルページアプリケーション用の追加構成が必要なため、シングルページアプリケーションセクションが特に役立つと思われます。

Spring Security 5 アプリケーションの移行の詳細については、マイグレーションの章のエクスプロイト保護セクションを参照してください。

CsrfToken の永続化

CsrfToken は CsrfTokenRepository を使用して永続化されます。

デフォルトでは、HttpSessionCsrfTokenRepository はセッション内のトークンの保存に使用されます。Spring Security は、トークンを Cookie に保存するための CookieCsrfTokenRepository も提供します。独自の実装を指定して、好きな場所にトークンを保存することもできます。

HttpSessionCsrfTokenRepository を使用する

デフォルトでは、Spring Security は HttpSessionCsrfTokenRepository (Javadoc) を使用して予期される CSRF トークンを HttpSession に保存するため、追加のコードは必要ありません。

HttpSessionCsrfTokenRepository は、デフォルトで X-CSRF-TOKEN という名前の HTTP リクエストヘッダーまたはリクエストパラメーター _csrf からトークンを読み取ります。

次の構成を使用して、デフォルト構成を明示的に指定できます。

HttpSessionCsrfTokenRepository を構成する
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRepository(new HttpSessionCsrfTokenRepository())
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRepository = HttpSessionCsrfTokenRepository()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
	class="org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository"/>

CsrfToken を Cookie に保存して、CookieCsrfTokenRepository (Javadoc) を使用する JavaScript ベースのアプリケーションをサポートできます。

CookieCsrfTokenRepository は、デフォルトで XSRF-TOKEN という名前の Cookie に書き込み、X-XSRF-TOKEN という名前の HTTP リクエストヘッダーまたはリクエストパラメーター _csrf からそれを読み取ります。これらのデフォルトは、Angular およびその前身である AngularJS (英語) からのものです。

このトピックに関する最新情報については、クロスサイトリクエストフォージェリ (XSRF) からの保護 (英語) ガイドおよび HttpClientXsrfModule (英語) を参照してください。

次の構成を使用して CookieCsrfTokenRepository を構成できます。

この例では、HttpOnly を false に明示的に設定します。これは、JavaScript フレームワーク (Angular など) に読み取らせるために必要です。JavaScript で Cookie を直接読み取る機能が必要ない場合は、セキュリティを向上させるために HttpOnly を省略する (代わりに new CookieCsrfTokenRepository() を使用する) ことをお勧めします

CsrfTokenRepository のカスタマイズ

カスタム CsrfTokenRepository (Javadoc) を実装したい場合があります。

CsrfTokenRepository インターフェースを実装したら、次の構成でそれを使用するように Spring Security を構成できます。

カスタム CsrfTokenRepository の構成
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRepository(new CustomCsrfTokenRepository())
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRepository = CustomCsrfTokenRepository()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
	class="example.CustomCsrfTokenRepository"/>

CsrfToken の取り扱いについて

CsrfToken は、CsrfTokenRequestHandler を使用するアプリケーションで使用できるようになります。このコンポーネントは、HTTP ヘッダーまたはリクエストパラメーターから CsrfToken を解決するロールも果たします。

デフォルトでは、XorCsrfTokenRequestAttributeHandler は CsrfTokenBREACH [Wikipedia] (英語) 保護を提供するために使用されます。Spring Security は、BREACH 保護をオプトアウトするための CsrfTokenRequestAttributeHandler も提供します。独自の実装を指定して、トークンの処理と解決の戦略をカスタマイズすることもできます。

XorCsrfTokenRequestAttributeHandler を使用する (BREACH)

XorCsrfTokenRequestAttributeHandler は、CsrfToken を _csrf と呼ばれる HttpServletRequest 属性として使用できるようにし、さらに BREACH [Wikipedia] (英語) の保護を提供します。

CsrfToken は、CsrfToken.class.getName() という名前を使用してリクエスト属性としても使用できます。この名前は構成できませんが、_csrf という名前は XorCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName を使用して変更できます。

この実装では、リクエストのトークン値もリクエストヘッダー (デフォルトでは X-CSRF-TOKEN または X-XSRF-TOKEN のいずれか) またはリクエストパラメーター (デフォルトでは _csrf ) として解決されます。

BREACH 保護は、ランダム性を CSRF トークン値にエンコードすることで提供され、返される CsrfToken がリクエストごとに変更されるようにします。後でトークンがヘッダー値またはリクエストパラメーターとして解決されると、デコードされて未処理のトークンが取得され、永続化された CsrfToken と比較されます。

Spring Security はデフォルトで CSRF トークンを BREACH 攻撃から保護するため、追加のコードは必要ありません。次の構成を使用して、デフォルト構成を明示的に指定できます。

違反保護を構成する
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
	class="org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler"/>

CsrfTokenRequestAttributeHandler を使用する

CsrfTokenRequestAttributeHandler は、CsrfToken を _csrf と呼ばれる HttpServletRequest 属性として使用できるようにします。

CsrfToken は、CsrfToken.class.getName() という名前を使用してリクエスト属性としても使用できます。この名前は構成できませんが、_csrf という名前は CsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName を使用して変更できます。

この実装では、リクエストのトークン値もリクエストヘッダー (デフォルトでは X-CSRF-TOKEN または X-XSRF-TOKEN のいずれか) またはリクエストパラメーター (デフォルトでは _csrf ) として解決されます。

CsrfTokenRequestAttributeHandler の主な用途は、CsrfToken の BREACH 保護をオプトアウトすることです。これは、次の構成を使用して構成できます。

BREACH 保護のオプトアウト
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
	class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler"/>

CsrfTokenRequestHandler のカスタマイズ

CsrfTokenRequestHandler インターフェースを実装して、トークンの処理と解決の戦略をカスタマイズできます。

CsrfTokenRequestHandler インターフェースは、リクエスト処理をカスタマイズするラムダ式を使用して実装できる @FunctionalInterface です。リクエストからトークンを解決する方法をカスタマイズするには、完全なインターフェースを実装する必要があります。委譲を使用してトークンの処理と解決のためのカスタム戦略を実装する例については、"シングルページアプリケーション用に CSRF を構成する" を参照してください。

CsrfTokenRequestHandler インターフェースを実装したら、次の構成でそれを使用するように Spring Security を構成できます。

カスタム CsrfTokenRequestHandler の構成
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRequestHandler(new CustomCsrfTokenRequestHandler())
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRequestHandler = CustomCsrfTokenRequestHandler()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
	class="example.CustomCsrfTokenRequestHandler"/>

CsrfToken の遅延ロード

デフォルトでは、Spring Security は必要になるまで CsrfToken のロードを延期します。

CsrfToken は、POST などの安全でない HTTP メソッドを使用してリクエストが行われるたびに必要です。さらに、CSRF トークンの非表示の <input> を含む <form> タグを含む Web ページなど、トークンをレスポンスにレンダリングするリクエストで必要になります。

Spring Security はデフォルトで CsrfToken を HttpSession にも格納するため、遅延 CSRF トークンにより、リクエストごとにセッションをロードする必要がなくなり、パフォーマンスが向上します。

遅延トークンをオプトアウトして、すべてのリクエストで CsrfToken をロードするようにしたい場合は、次の構成で行うことができます。

遅延 CSRF トークンのオプトアウト
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		XorCsrfTokenRequestAttributeHandler requestHandler = new XorCsrfTokenRequestAttributeHandler();
		// set the name of the attribute the CsrfToken will be populated on
		requestHandler.setCsrfRequestAttributeName(null);
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRequestHandler(requestHandler)
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        val requestHandler = XorCsrfTokenRequestAttributeHandler()
        // set the name of the attribute the CsrfToken will be populated on
        requestHandler.setCsrfRequestAttributeName(null)
        http {
            // ...
            csrf {
                csrfTokenRequestHandler = requestHandler
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
	class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler">
	<b:property name="csrfRequestAttributeName">
		<b:null/>
	</b:property>
</b:bean>

csrfRequestAttributeName を null に設定することにより、最初に CsrfToken をロードして、使用する属性名を決定する必要があります。これにより、リクエストごとに CsrfToken がロードされます。

CSRF 保護との統合

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

次のセクションでは、フロントエンドまたはクライアントアプリケーションを CSRF で保護されたバックエンドアプリケーションと統合できるさまざまな方法について説明します。

HTML フォーム

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

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

次のビューテクノロジーでは、POST などの安全でない HTTP メソッドを含むフォームに実際の CSRF トークンが自動的に含まれます。

これらのオプションが利用できない場合は、CsrfToken が  _csrf という名前の HttpServletRequest 属性として公開されるという事実を利用できます。次の例では、JSP を使用してこれを実行します。

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

JavaScript アプリケーション

JavaScript アプリケーションは通常、HTML ではなく JSON を使用します。JSON を使用する場合は、リクエストパラメーターの代わりに HTTP リクエストヘッダー内で CSRF トークンを送信できます。

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

シングルページアプリケーション (SPA) を Spring Security の CSRF 保護と統合する場合は、BREACH 保護と遅延トークンについて特別な考慮事項があります。完全な構成例は次のセクションで説明します。

次のセクションでは、さまざまな型の JavaScript アプリケーションについて説明します。

シングルページアプリケーション

シングルページアプリケーション (SPA) を Spring Security の CSRF 保護と統合するには、特別な考慮事項があります。

Spring Security はデフォルトで CsrfToken の BREACH 保護を提供することを思い出してください。予期される CSRF トークンを Cookie に保存する場合、JavaScript アプリケーションはプレーントークン値にのみアクセスでき、エンコードされた値にはアクセスできません。実際のトークン値を解決するためにカスタマイズされたリクエストハンドラーを提供する必要があります。

さらに、CSRF トークンを保存する Cookie は、認証成功およびログアウト成功時にクリアされます。Spring Security はデフォルトで新しい CSRF トークンのロードを延期し、新しい Cookie を返すには追加の作業が必要です。

CsrfAuthenticationStrategy (Javadoc) CsrfLogoutHandler (Javadoc) は以前のトークンをクリアするため、認証の成功とログアウトの成功後にトークンをリフレッシュする必要があります。クライアントアプリケーションは、新しいトークンを取得しない限り、POST などの安全でない HTTP リクエストを実行できません。

シングルページアプリケーションを Spring Security と簡単に統合するには、次の構成を使用できます。

シングルページアプリケーション用に CSRF を構成する
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())   (1)
				.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())            (2)
			)
			.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); (3)
		return http.build();
	}
}

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);
	}
}

final class CsrfCookieFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
		// Render the token value to a cookie by causing the deferred token to be loaded
		csrfToken.getToken();

		filterChain.doFilter(request, response);
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse()    (1)
                csrfTokenRequestHandler = SpaCsrfTokenRequestHandler()                 (2)
            }
        }
        http.addFilterAfter(CsrfCookieFilter(), BasicAuthenticationFilter::class.java) (3)
        return http.build()
    }
}

class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() {
    private val delegate: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()

    override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
        /*
         * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
         * the CsrfToken when it is rendered in the response body.
         */
        delegate.handle(request, response, csrfToken)
    }

    override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
        /*
         * 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.
         */
        return if (StringUtils.hasText(request.getHeader(csrfToken.headerName))) {
            super.resolveCsrfTokenValue(request, csrfToken)
        } else {
            /*
             * 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.
             */
            delegate.resolveCsrfTokenValue(request, csrfToken)
        }
    }
}

class CsrfCookieFilter : OncePerRequestFilter() {

    @Throws(ServletException::class, IOException::class)
    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
        val csrfToken = request.getAttribute("_csrf") as CsrfToken
        // Render the token value to a cookie by causing the deferred token to be loaded
        csrfToken.token
        filterChain.doFilter(request, response)
    }
}
<http>
	<!-- ... -->
	<csrf
		token-repository-ref="tokenRepository"                        (1)
		request-handler-ref="requestHandler"/>                        (2)
	<custom-filter ref="csrfCookieFilter" after="BASIC_AUTH_FILTER"/> (3)
</http>
<b:bean id="tokenRepository"
	class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
	p:cookieHttpOnly="false"/>
<b:bean id="requestHandler"
	class="example.SpaCsrfTokenRequestHandler"/>
<b:bean id="csrfCookieFilter"
	class="example.CsrfCookieFilter"/>
1JavaScript アプリケーションが Cookie を読み取れるように、HttpOnly を false に設定して CookieCsrfTokenRepository を構成します。
2CSRF トークンが HTTP リクエストヘッダー (X-XSRF-TOKEN) であるかリクエストパラメーター (_csrf) であるかに基づいて解決するカスタム CsrfTokenRequestHandler を構成します。
3 すべてのリクエストで CsrfToken をロードするようにカスタム Filter を構成します。これにより、必要に応じて新しい Cookie が返されます。

複数ページのアプリケーション

JavaScript が各ページに読み込まれるマルチページアプリケーションの場合、Cookie で CSRF トークンを公開する代わりに、CSRF トークンを meta タグ内に含めることができます。HTML は次のようになります。

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

リクエストに CSRF トークンを含めるには、CsrfToken が  _csrf という名前の HttpServletRequest 属性として公開されるという事実を利用できます。次の例では、JSP を使用してこれを実行します。

リクエスト属性を持つ HTML メタタグ内の CSRF トークン
<html>
<head>
	<meta name="_csrf" content="${_csrf.token}"/>
	<!-- default header name is X-CSRF-TOKEN -->
	<meta name="_csrf_header" content="${_csrf.headerName}"/>
	<!-- ... -->
</head>
<!-- ... -->
</html>

メタタグに 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);
	});
});

その他の JavaScript アプリケーション

JavaScript アプリケーションの別のオプションは、HTTP レスポンスヘッダーに CSRF トークンを含めることです。

これを実現する 1 つの方法は、@ControllerAdvice を CsrfTokenArgumentResolver とともに使用することです。以下は、アプリケーション内のすべてのコントローラーエンドポイントに適用される @ControllerAdvice の例です。

HTTP レスポンスヘッダーの CSRF トークン
  • Java

  • Kotlin

@ControllerAdvice
public class CsrfControllerAdvice {

	@ModelAttribute
	public void getCsrfToken(HttpServletResponse response, CsrfToken csrfToken) {
		response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
	}

}
@ControllerAdvice
class CsrfControllerAdvice {

	@ModelAttribute
	fun getCsrfToken(response: HttpServletResponse, csrfToken: CsrfToken) {
		response.setHeader(csrfToken.headerName, csrfToken.token)
	}

}

この @ControllerAdvice はアプリケーション内のすべてのエンドポイントに適用されるため、リクエストごとに CSRF トークンが読み込まれることになり、HttpSessionCsrfTokenRepository を使用する場合の遅延トークンの利点が無効になる可能性があります。ただし、CookieCsrfTokenRepository を使用する場合、これは通常課題になりません。

コントローラーエンドポイントとコントローラーアドバイスは、Spring Security フィルターチェーンの後に呼び出されることを覚えておくことが重要です。これは、リクエストがフィルターチェーン を通過してアプリケーションに渡される場合にのみ、この @ControllerAdvice が適用されることを意味します。HttpServletResponse に事前にアクセスするためにフィルターチェーン にフィルターを追加する例については、シングルページアプリケーションの構成を参照してください。

CSRF トークンは、コントローラーのアドバイスが適用されるカスタムエンドポイントのレスポンスヘッダー (デフォルトでは X-CSRF-TOKEN または X-XSRF-TOKEN) で使用できるようになります。バックエンドへのリクエストはすべて、レスポンスからトークンを取得するために使用でき、後続のリクエストでは同じ名前のリクエストヘッダーにトークンを含めることができます。

モバイルアプリケーション

JavaScript アプリケーションと同様、モバイルアプリケーションは通常、HTML ではなく JSON を使用します。ブラウザートラフィックを処理しないバックエンドアプリケーションは、CSRF を無効にすることを選択する場合があります。その場合、追加の作業は必要ありません。

ただし、ブラウザートラフィックも処理するため、依然として CSRF 保護が必要なバックエンドアプリケーションは、Cookie ではなくセッションに CsrfToken を保存し続ける可能性があります。

この場合、バックエンドと統合するための一般的なパターンは、/csrf エンドポイントを公開して、フロントエンド (モバイルまたはブラウザークライアント) がオンデマンドで CSRF トークンをリクエストできるようにすることです。このパターンを使用する利点は、CSRF トークンを引き続き延期でき、リクエストで CSRF 保護が必要な場合にのみセッションからロードする必要があることです。カスタムエンドポイントの使用は、クライアントアプリケーションが明示的なリクエストを発行することで、(必要に応じて) 新しいトークンをオンデマンドで生成することをリクエストできることも意味します。

このパターンは、モバイルアプリケーションだけでなく、CSRF 保護を必要とするあらゆる種類のアプリケーションに使用できます。このようなケースでは通常、このアプローチは必要ありませんが、CSRF で保護されたバックエンドと統合するためのもう 1 つのオプションです。

以下は、CsrfTokenArgumentResolver を使用する /csrf エンドポイントの例です。

/csrf エンドポイント
  • Java

  • Kotlin

@RestController
public class CsrfController {

    @GetMapping("/csrf")
    public CsrfToken csrf(CsrfToken csrfToken) {
        return csrfToken;
    }

}
@RestController
class CsrfController {

    @GetMapping("/csrf")
    fun csrf(csrfToken: CsrfToken): CsrfToken {
        return csrfToken
    }

}

サーバーで認証する前に上記のエンドポイントが必要な場合は、.requestMatchers("/csrf").permitAll() の追加を検討してください。

このエンドポイントは、アプリケーションの起動時または初期化時 (ロード時など)、および認証成功およびログアウト成功後にも呼び出されて、CSRF トークンを取得する必要があります。

CsrfAuthenticationStrategy (Javadoc) CsrfLogoutHandler (Javadoc) は以前のトークンをクリアするため、認証の成功とログアウトの成功後にトークンをリフレッシュする必要があります。クライアントアプリケーションは、新しいトークンを取得しない限り、POST などの安全でない HTTP リクエストを実行できません。

CSRF トークンを取得したら、それを HTTP リクエストヘッダー (デフォルトでは X-CSRF-TOKEN または X-XSRF-TOKEN のいずれか) として自分で含める必要があります。

ハンドル AccessDeniedException

InvalidCsrfTokenException などの AccessDeniedException を処理するには、これらの例外を任意の方法で処理するように Spring Security を構成できます。例: 次の構成を使用して、カスタムのアクセス拒否ページを構成できます。

AccessDeniedHandler を構成する
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.exceptionHandling((exceptionHandling) -> exceptionHandling
				.accessDeniedPage("/access-denied")
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            exceptionHandling {
                accessDeniedPage = "/access-denied"
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<access-denied-handler error-page="/access-denied"/>
</http>

CSRF テスト

次のように、Spring Security のテストサポートCsrfRequestPostProcessor を使用して CSRF 保護をテストできます。

CSRF 保護をテストする
  • Java

  • Kotlin

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SecurityConfig.class)
@WebAppConfiguration
public class CsrfTests {

	private MockMvc mockMvc;

	@BeforeEach
	public void setUp(WebApplicationContext applicationContext) {
		this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
			.apply(springSecurity())
			.build();
	}

	@Test
	public void loginWhenValidCsrfTokenThenSuccess() throws Exception {
		this.mockMvc.perform(post("/login").with(csrf())
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().is3xxRedirection())
			.andExpect(header().string(HttpHeaders.LOCATION, "/"));
	}

	@Test
	public void loginWhenInvalidCsrfTokenThenForbidden() throws Exception {
		this.mockMvc.perform(post("/login").with(csrf().useInvalidToken())
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().isForbidden());
	}

	@Test
	public void loginWhenMissingCsrfTokenThenForbidden() throws Exception {
		this.mockMvc.perform(post("/login")
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().isForbidden());
	}

	@Test
	@WithMockUser
	public void logoutWhenValidCsrfTokenThenSuccess() throws Exception {
		this.mockMvc.perform(post("/logout").with(csrf())
				.accept(MediaType.TEXT_HTML))
			.andExpect(status().is3xxRedirection())
			.andExpect(header().string(HttpHeaders.LOCATION, "/login?logout"));
	}
}
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*

@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [SecurityConfig::class])
@WebAppConfiguration
class CsrfTests {
	private lateinit var mockMvc: MockMvc

	@BeforeEach
	fun setUp(applicationContext: WebApplicationContext) {
		mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
			.apply<DefaultMockMvcBuilder>(springSecurity())
			.build()
	}

	@Test
	fun loginWhenValidCsrfTokenThenSuccess() {
		mockMvc.perform(post("/login").with(csrf())
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().is3xxRedirection)
			.andExpect(header().string(HttpHeaders.LOCATION, "/"))
	}

	@Test
	fun loginWhenInvalidCsrfTokenThenForbidden() {
		mockMvc.perform(post("/login").with(csrf().useInvalidToken())
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().isForbidden)
	}

	@Test
	fun loginWhenMissingCsrfTokenThenForbidden() {
		mockMvc.perform(post("/login")
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().isForbidden)
	}

	@Test
	@WithMockUser
	@Throws(Exception::class)
	fun logoutWhenValidCsrfTokenThenSuccess() {
		mockMvc.perform(post("/logout").with(csrf())
				.accept(MediaType.TEXT_HTML))
			.andExpect(status().is3xxRedirection)
			.andExpect(header().string(HttpHeaders.LOCATION, "/login?logout"))
	}
}

CSRF 保護を無効にする

デフォルトでは、CSRF 保護が有効になっており、バックエンドとの統合とアプリケーションのテストに影響します。CSRF 保護を無効にする前に、それがアプリケーションにとって意味があるかどうかを検討してください。

次の例のように、特定のエンドポイントだけが CSRF 保護を必要としないかどうかを検討し、無視ルールを設定することもできます。

リクエストの無視
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // ...
            .csrf((csrf) -> csrf
                .ignoringRequestMatchers("/api/*")
            );
        return http.build();
    }
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                ignoringRequestMatchers("/api/*")
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf request-matcher-ref="csrfMatcher"/>
</http>
<b:bean id="csrfMatcher"
    class="org.springframework.security.web.util.matcher.AndRequestMatcher">
    <b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
    <b:constructor-arg>
        <b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
            <b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
                <b:constructor-arg value="/api/*"/>
            </b:bean>
        </b:bean>
    </b:constructor-arg>
</b:bean>

CSRF 保護を無効にする必要がある場合は、次の構成を使用して無効にすることができます。

CSRF を無効にする
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf.disable());
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                disable()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf disabled="true"/>
</http>

CSRF の考慮事項

CSRF 攻撃に対する保護を実装する場合は、特別な考慮事項がいくつかあります。このセクションでは、サーブレット環境に関連する考慮事項について説明します。より一般的な説明については、CSRF の考慮事項を参照してください。

ログイン

ログイン試行の偽造から保護するために、ログインリクエストに CSRF をリクエストすることが重要です。Spring Security のサーブレットサポートは、これをそのまま実行します。

ログアウト

偽造されたログアウト試行から保護するには、ログアウトリクエストに CSRF をリクエストすることが重要です。CSRF 保護が有効になっている場合 (デフォルト)、Spring Security の LogoutFilter は HTTP POST リクエストのみを処理します。これにより、ログアウトには CSRF トークンが必要となり、悪意のあるユーザーがユーザーを強制的にログアウトすることができなくなります。

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

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

任意の HTTP メソッドでログアウトする
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.logout((logout) -> logout
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            logout {
                logoutRequestMatcher = AntPathRequestMatcher("/logout")
            }
        }
        return http.build()
    }
}

詳細については、ログアウトの章を参照してください。

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

デフォルトでは、Spring Security は HttpSessionCsrfTokenRepository を使用して CSRF トークンを HttpSession に保存します。これにより、セッションが期限切れになり、検証する CSRF トークンが残らないという状況が発生する可能性があります。

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

CSRF トークンのストレージを Cookie に変更することができます。詳細については、CookieCsrfTokenRepository を使用するのセクションを参照してください。

トークンの有効期限が切れた場合は、カスタム AccessDeniedHandler を指定してトークンの処理方法をカスタマイズすることができます。カスタム AccessDeniedHandler は、InvalidCsrfTokenException を任意の方法で処理できます。

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

マルチパートリクエスト (ファイルアップロード) を CSRF 攻撃から保護することが、鶏が先か卵が先か [Wikipedia] の課題を引き起こす仕組みについてはすでに説明しました。JavaScript が利用可能な場合は、課題を回避するために HTTP リクエストヘッダーに CSRF トークンを含めること をお勧めします

JavaScript が使用できない場合、次のセクションでは、サーブレットアプリケーション内の本文および URL に CSRF トークンを配置するためのオプションについて説明します。

Spring でマルチパートフォームを使用する方法の詳細については、Spring リファレンスのマルチパートリゾルバーセクションと MultipartFilter javadoc を参照してください。

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

CSRF トークンを本体に配置することのトレードオフについてはすでに説明しました。このセクションでは、本体から CSRF を読み取るように Spring Security を構成する方法について説明します。

本体から CSRF トークンを読み取るには、Spring Security フィルターの前に MultipartFilter を指定します。Spring Security フィルターの前に MultipartFilter を指定することは、MultipartFilter を呼び出すための認可がないことを意味します。つまり、誰でもサーバーに一時ファイルを置くことができます。ただし、アプリケーションによって処理されるファイルを送信できるのは、認可されたユーザーのみです。一般に、一時ファイルのアップロードによるほとんどのサーバーへの影響はごくわずかであるため、これが推奨されるアプローチです。

MultipartFilter を構成する
  • Java

  • Kotlin

  • XML

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

	@Override
	protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
		insertFilters(servletContext, new MultipartFilter());
	}
}
class SecurityApplicationInitializer : AbstractSecurityWebApplicationInitializer() {
    override fun beforeSpringSecurityFilterChain(servletContext: ServletContext?) {
        insertFilters(servletContext, MultipartFilter())
    }
}
<filter>
	<filter-name>MultipartFilter</filter-name>
	<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
	<filter-name>springSecurityFilterChain</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>MultipartFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
	<filter-name>springSecurityFilterChain</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

XML 構成で MultipartFilter が Spring Security フィルターの前に指定されるようにするには、MultipartFilter の <filter-mapping> 要素が web.xml ファイル内の springSecurityFilterChain の前に配置されるようにします。

URL に CSRF トークンを含める

権限のないユーザーに一時ファイルのアップロードを認可しない場合は、MultipartFilter を Spring Security フィルターの後に配置し、フォームのアクション属性にクエリパラメーターとして CSRF を含めることもできます。CsrfToken は  _csrf という名前の HttpServletRequest 属性として公開されているため、それを使用して CSRF トークンを含む action を作成できます。次の例では、JSP を使用してこれを実行します。

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

HiddenHttpMethodFilter

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

Spring のサーブレットサポートでは、HTTP メソッドのオーバーライドは HiddenHttpMethodFilter (Javadoc) を使用して行われます。詳細については、リファレンスドキュメントの HTTP メソッド変換セクションを参照してください。

参考文献

CSRF 保護について確認したため、セキュアヘッダーHTTP ファイアウォールなどのエクスプロイト保護についてさらに学習するか、アプリケーションのテスト方法の学習に進むことを検討してください。