9.5. CSRF対策

9.5.1. Overview

本節では、Spring Securityが提供しているCross site request forgeries(以下、CSRFと略す)対策の機能について説明する。

CSRFとは、Webサイトにスクリプトや自動転送(HTTPリダイレクト)を実装することにより、 ログイン済みの別のWebサイト上で、ユーザーが意図しない何らかの操作を行わせる攻撃手法のことである。

サーバ側でCSRFを防ぐには、以下の方法が知られている。

  • 秘密情報(トークン)の埋め込み
  • パスワードの再入力
  • Refererのチェック

CSRF対策機能は、攻撃者が用意したWebページから送られてくる偽造リクエストを不正なリクエストとして扱うための機能である。 CSRF対策が行われていないWebアプリケーションを利用すると、以下のような方法で攻撃を受ける可能性がある。

  • 利用者は、CSRF対策が行われていないWebアプリケーションにログインする。
  • 利用者は、攻撃者からの巧みな誘導によって、攻撃者が用意したWebページを開いてしまう。
  • 攻撃者が用意したWebページは、フォームの自動送信などのテクニックを使用して、偽造したリクエストをCSRF対策が行われていないWebアプリケーションに対して送信する。
  • CSRF対策が行われていないWebアプリケーションは、攻撃者が偽造したリクエストを正規のリクエストとして処理してしまう。

Tip

OWASP[1]では、トークンパターンを使用する方法が推奨されている。

[1]Open Web Application Security Projectの略称であり、信頼できるアプリケーションや、セキュリティに関する 効果的なアプローチなどを検証、提唱する、国際的な非営利団体である。 https://www.owasp.org/index.php/Main_Page

Note

ログイン時におけるCSRF対策

CSRF対策はログイン中のリクエストだけではなく、ログイン処理でも行う必要がある。 ログイン処理に対してCSRF対策を怠った場合、攻撃者が用意したアカウントを使って知らぬ間にログインさせられ、ログイン中に行った操作履歴などを盗まれる可能性がある。

Warning

マルチパートリクエスト(ファイルアップロード)時におけるCSRF対策

ファイルアップロード時のCSRF対策については、ファイルアップロード Servlet Filterの設定を留意されたい。

9.5.1.1. Spring SecurityのCSRF対策

Spring Securityは、セッション単位にランダムに生成される固定トークン値(CSRFトークン)を払い出し、払い出されたCSRFトークンをリクエストパラメータ(HTMLフォームのhidden項目)として送信する。 これにより正規のWebページからのリクエストなのか、攻撃者が用意したWebページからのリクエストなのかを判断する仕組みを採用している。

../_images/Csrf.png

Spring SecurityのCSRF対策の仕組み

項番 説明
(1)
クライアントは、HTTPのGETメソッドを使用してアプリケーションサーバにアクセスする。
(2)
Spring Securityは、CSRFトークンを生成しHTTPセッションに格納する。
生成したCSRFトークンは、HTMLフォームのhiddenタグを使ってクライアントと連携する。
(3)
クライアントは、HTMLフォーム内のボタンを押下してアプリケーションサーバーにリクエストを送信する。
HTMLフォーム内のhidden項目にCSRFトークンが埋め込まれているため、CSRFトークン値はリクエストパラメータとして送信される。
(4)
Spring Securityは、HTTPのPOSTメソッドを使ってアクセスされた際は、リクエストパラメータに指定されたCSRFトークン値とHTTPセッション内に保持しているCSRFトークン値が同じ値であることをチェックする。
トークン値が一致しない場合は、不正なリクエスト(攻撃者からのリクエスト)としてエラーを発生させる。
(5)
クライアントは、HTTPのGETメソッドを使用してアプリケーションサーバにアクセスする。
(6)
Spring Securityは、GETメソッドを使ってアクセスされた際は、CSRFトークン値のチェックは行わない。

Note

Ajax使用時のCSRFトークン

Spring Securityは、リクエストヘッダにCSRFトークン値を設定することができるため、Ajax向けのリクエストなどに対してCSRF対策を行うことが可能である。

9.5.1.1.1. トークンチェックの対象リクエスト

Spring Securityのデフォルト実装では、以下のHTTPメソッドを使用したリクエストに対して、CSRFトークンチェックを行う。

  • POST
  • PUT
  • DELETE
  • PATCH

Note

CSRFトークンチェックを行わない理由

GET, HEAD, OPTIONS, TRACE メソッドがチェック対象外となっている理由は、これらのメソッドがアプリケーションの状態を変更するようなリクエストを実行するためのメソッドではないためである。

9.5.2. How to use

9.5.2.1. CSRF対策機能の適用

CSRFトークン用のRequestDataValueProcessor実装クラスを利用し、Thymeleafの th:action属性を使うことで、自動的にCSRFトークンをhidden項目に埋め込むことができる。

  • spring-mvc.xmlの設定例
<bean id="requestDataValueProcessor"
    class="org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor"> <!-- (1)  -->
    <constructor-arg>
        <util:list>
            <bean
                class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor" /> <!-- (2)  -->
            <bean
                class="org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor" />
        </util:list>
    </constructor-arg>
</bean>
項番 説明
(1)
共通ライブラリから提供されている、org.springframework.web.servlet.support.RequestDataValueProcessorを複数定義可能な
org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessorをbean定義する。
(2)
コンストラクタの第1引数に、org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessorのbean定義を設定する。

Spring Security 4.0からは、上記設定により、デフォルトでCSRF対策機能が有効となる。このため、CSRF対策機能を適用したくない場合は、明示的に無効化する必要がある。

CSRF対策機能を使用しない場合は、以下のようなbean定義を行う。

  • spring-security.xmlの定義例
<sec:http>
    <!-- omitted -->
    <sec:csrf disabled="true"/> <!-- disabled属性にtrueを設定して無効化 -->
    <!-- omitted -->
</sec:http>

9.5.2.2. CSRFトークン値の連携

Spring Securityは、CSRFトークン値をクライアントとサーバー間で連携する方法として、以下の2種類の方法を提供している。

  • HTMLフォームのhidden項目としてCSRFトークン値を出力し、リクエストパラメータとして連携する
  • HTMLのmetaタグとしてCSRFトークンの情報を出力し、Ajax通信時にリクエストヘッダにトークン値を設定して連携する

9.5.2.2.1. Spring MVCを使用した連携

Spring Securityは、Spring MVCと連携するためのコンポーネントをいくつか提供している。 ここでは、CSRF対策機能と連携するためのコンポーネントの使い方を説明する。

9.5.2.2.1.1. hidden項目の自動出力

HTMLフォームを作成する際は、以下のようにThymeleafのテンプレートHTMLを実装する。

  • テンプレートHTMLの実装例
<form th:action="@{/login}" method="post"> <!-- (1) -->
    <!-- omitted -->
</form>
項番 説明
(1)
HTMLフォームを作成する際は、Thymeleafの th:action 属性を使用する。

Thymeleafの th:action 属性を使うと、以下のようなHTMLフォームが作成される。

  • HTMLの出力例
<form action="/login" method="post">
    <!-- Spring MVCの機能と連携して出力されたCSRFトークン値のhidden項目 -->
    <input type="hidden"
        name="_csrf" value="63845086-6b57-4261-8440-97a3c6fa6b99" />
    <!-- omitted -->
</form>

Tip

出力されるCSRFトークンチェック値

Spring 4上でCsrfRequestDataValueProcessorを使用すると、th:action属性が付与された<form>タグのmethod属性に指定した値がCSRFトークンチェック対象の HTTPメソッド(Spring Securityのデフォルト実装ではGET,HEAD,TRACE,OPTIONS以外のHTTPメソッド)と一致する場合に限り、CSRFトークンが埋め込まれた<input type="hidden">タグが出力される。

例えば、以下の例のように method属性にGETメソッドを指定した場合は、CSRFトークンが埋め込まれた<input type="hidden">タグは出力されない。

<form method="GET" th:object="${xxxForm}" th:action="@{...}">
    <!--/* ... */--!>
</form>

これは、OWASP Top 10で説明されている、

The unique token can also be included in the URL itself, or a URL parameter. However, such placement runs a greater risk that the URL will be exposed to an attacker, thus compromising the secret token.

に対応している事を意味しており、セキュアなWebアプリケーション構築の手助けとなる。

なお、<form>要素にmethod属性が指定されていない場合、HTML5標準ではGETメソッドとして処理される。このため、CSRF対策機能を使用する場合、明示的にmethod属性にpostを指定する必要がある。

Note

自動的にCSRFトークンを埋め込みたいが、action属性を付与したくない場合

リクエストURLを生成する」で解説する「現在のパスからの相対パス」を利用することで、リクエストマッピングのパスが異なる複数のコントローラで同じテンプレートHTMLを使いまわすことが可能である。 「現在のパスからの相対パス」を使用すると、必ずページを取得したパスから派生する別のパスを指定する必要があるように見えるが、th:action属性の値を指定しないことで、出力されるaction属性の値が空になり、ページを取得したのと同じパスに対してリクエストを送信することが可能となる。 (一般的なブラウザでは、action属性の値を空にすると、action属性を付与していないのと同じ動作となる。)

これを利用して、自動的にCSRトークンをhidden要素に埋め込みたいが、action属性を付与したくない(=ページを取得したのと同じパスに対してリクエストを送信したい)という要件を実現することが可能である。

以下に、th:action属性の値を指定しない例を示す。

<form th:action method="post">
    <!--/* ... */--!>
</form>

9.5.2.2.2. Ajax使用時の連携

Ajaxを使ってリクエストを送信する場合は、HTMLのmetaタグとしてCSRFトークンの情報を出力し、metaタグから取得したトークン値をAjax通信時のリクエストヘッダに設定して連携する。

まず、HTMLのmetaタグにCSRFトークンの情報を出力する。

  • テンプレートHTMLの実装例
<head>
    <!-- omitted -->
    <meta name="_csrf_parameter" th:content="${_csrf.parameterName}" /> <!-- (1) -->
    <meta name="_csrf_header" th:content="${_csrf.headerName}" /> <!-- (1) -->
    <meta name="_csrf" th:content="${_csrf.token}" /> <!-- (1) -->
    <!-- omitted -->
</head>
項番 説明
(1)
HTMLの<head>要素内に、CSRFトークンの情報を埋め込んだ <meta> 要素を設定する。

このように <meta> タグを設定すると、以下のように出力される。 デフォルトでは、CSRFトークン値を連携するためのリクエストヘッダ名はX-CSRF-TOKENとなる。

  • HTMLの出力例
<head>
    <!-- omitted -->
    <meta name="_csrf_parameter" content="_csrf" />
    <meta name="_csrf_header" content="X-CSRF-TOKEN" /> <!-- ヘッダ名 -->
    <meta name="_csrf"
          content="63845086-6b57-4261-8440-97a3c6fa6b99" /> <!-- トークン値 -->
    <!-- omitted -->
</head>

つぎに、JavaScriptを使ってmetaタグからCSRFトークンの情報を取得し、Ajax通信時のリクエストヘッダ にCSRFトークン値を設定する。(ここではjQueryを使った実装例となっている)

  • JavaScriptの実装例
$(function () {
    var headerName = $("meta[name='_csrf_header']").attr("content"); // (1)
    var tokenValue = $("meta[name='_csrf']").attr("content"); // (2)
    $(document).ajaxSend(function(e, xhr, options) {
        xhr.setRequestHeader(headerName, tokenValue); // (3)
    });
});
項番 説明
(1)
CSRFトークン値を連携するためのリクエストヘッダ名を取得する。
(2)
CSRFトークン値を取得する。
(3)
リクエストヘッダにCSRFトークン値を設定する。

9.5.2.3. トークンチェックエラー時の遷移先の制御

トークンチェックエラー時の遷移先の制御を行うためには、CSRFトークンチェックエラーに発生する例外である AccessDeniedExceptionをハンドリングして、その例外に対応した遷移先を指定する。

CSRFのトークンチェックエラー時に発生する例外は以下の通りである。

CSRFトークンチェックで使用される例外クラス
クラス名 説明
InvalidCsrfTokenException
クライアントから送られたトークン値と、サーバー側で保持しているトークン値が一致しない場合に使用する例外クラス(主に不正なリクエスト)。
MissingCsrfTokenException
サーバー側にトークン値が保存されていない場合に使用する例外クラス(主にセッション切れ)。

DelegatingAccessDeniedHandlerクラスを使用して上記の例外をハンドリングし、それぞれに AccessDeniedHandlerインタフェースの実装クラスを割り当てることで、例外毎の遷移先を設定することが可能である。

CSRFトークンチェックエラー時に専用のエラー画面に遷移させたい場合は、以下のようなBean定義を行う。(以下の定義例は、ブランクプロジェクトからの抜粋である)

  • spring-security.xmlの定義例
<sec:http>
    <!-- omitted -->
    <sec:access-denied-handler ref="accessDeniedHandler"/>  <!-- (1) -->
    <!-- omitted -->
</sec:http>

<bean id="accessDeniedHandler"
    class="org.springframework.security.web.access.DelegatingAccessDeniedHandler">  <!-- (2) -->
    <constructor-arg index="0">  <!-- (3) -->
        <map>
            <!-- (4) -->
            <entry
                key="org.springframework.security.web.csrf.InvalidCsrfTokenException">
                <bean
                    class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                    <property name="errorPage"
                        value="/common/error/invalidCsrfTokenError" />
                </bean>
            </entry>
            <!-- (5) -->
            <entry
                key="org.springframework.security.web.csrf.MissingCsrfTokenException">
                <bean
                    class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                    <property name="errorPage"
                        value="/common/error/missingCsrfTokenError" />
                </bean>
            </entry>
        </map>
    </constructor-arg>
    <!-- (6) -->
    <constructor-arg index="1">
        <bean
            class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
            <property name="errorPage"
                value="/common/error/accessDeniedError" />
        </bean>
    </constructor-arg>
</bean>
項番 説明
(1)
<sec:access-denied-handler>タグのref属性に、Exception毎の制御を行うためのAccessDeniedHandlerのBean名を指定する。
エラー時遷移先が全て同じ画面である場合は error-page 属性に遷移先を指定すればよい。
<sec:access-denied-handler>でハンドリングしない場合は、認可エラー時の遷移先を参照されたい。
(2)
DelegatingAccessDeniedHandlerを使用して、発生した例外( AccessDeniedExceptionサブクラス ) と例外ハンドラ( AccessDeniedHandler実装クラス )を定義する。
(3)
コンストラクタの第1引数で、個別に遷移先を指定したい例外( AccessDeniedExceptionサブクラス )と、対応する例外ハンドラ( AccessDeniedHandler実装クラス )をMap形式で定義する。
(4)
keyAccessDeniedExceptionのサブクラスを指定する。
value として、AccessDeniedHandlerの実装クラスである、 org.springframework.security.web.access.AccessDeniedHandlerImpl を指定する。
propertynameerrorPageを指定し、valueに表示するviewへ遷移するパスを指定する。
マッピングするExceptionに関しては、トークンチェックエラー時の遷移先の制御 を参照されたい。
(5)
(4)のExceptionと異なるExceptionを制御したい場合に定義する。
本例では InvalidCsrfTokenExceptionMissingCsrfTokenExceptionそれぞれに異なる遷移先を設定している。
(6)
コンストラクタの第2引数で、デフォルト例外((4)(5)で指定していない AccessDeniedExceptionのサブクラス)時の例外ハンドラ( AccessDeniedHandler実装クラス )と遷移先を指定する。

Note

無効なセッションを使ったリクエストの検知

セッション管理機能の「無効なセッションを使ったリクエストの検知」処理を有効にしている場合は、MissingCsrfTokenExceptionに対して「無効なセッションを使ったリクエストの検知」処理と連動するAccessDeniedHandlerインタフェースの実装クラスが適用される。

そのため、MissingCsrfTokenExceptionが発生すると、「無効なセッションを使ったリクエストの検知」処理を有効化する際に指定したパス(invalid-session-url)にリダイレクトする。

Note

ステータスコード403以外を返却したい場合

リクエストに含まれるCSRFトークンが一致しない場合に、ステータスコード403以外を返却したい場合は、org.springframework.security.web.access.AccessDeniedHandlerインタフェースを実装した、独自のAccessDeniedHandlerを作成する必要がある。