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ページからのリクエストなのかを判断する仕組みを採用している。
| 項番 | 説明 | 
|---|---|
| (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実装クラスを利用し、Springのタグライブラリの<form:form>タグを使うことで、自動的に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.2. HTMLフォーム使用時の連携¶
Spring MVCと連携 せずに、HTMLフォームを使用してCSRFトークン値を連携することも可能である。 HTMLフォームを使ってリクエストを送信する場合は、HTMLフォームのhidden項目としてCSRFトークン値を出力し、リクエストパラメータとして連携する。
- JSPの実装例
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<form action="<c:url value="/login" />" method="post">
    <!-- omitted -->
    <sec:csrfInput /> <!-- (1) -->
    <!-- omitted -->
</form>
| 項番 | 説明 | 
|---|---|
| (1) | HTMLの <form>要素の中に<sec:csrfInput>要素を指定する。 | 
Spring Securityから提供されている<sec:csrfInput>要素を指定すると、以下のようなhidden項目が出力される。
HTMLフォーム内にhidden項目を出力することで、CSRFトークン値がリクエストパラメータとして連携される。
デフォルトでは、CSRFトークン値を連携するためのリクエストパラメータ名は_csrfになる。
- HTMLの出力例
<form action="/login" method="post">
    <!-- omitted -->
    <!-- CSRFトークン値のhidden項目 -->
    <input type="hidden"
           name="_csrf"
           value="63845086-6b57-4261-8440-97a3c6fa6b99" />
    <!-- omitted -->
</form>
Warning
GETメソッド使用時の注意点
HTTPメソッドとしてGETを使用する場合、<sec:csrfInput>要素を指定しないこと。
<sec:csrfInput>要素を指定してしまうと、URLにCSRFトークン値が含まれてしまうため、CSRFトークン値が盗まれるリスクが高くなる。
9.5.2.2.3. Ajax使用時の連携¶
Ajaxを使ってリクエストを送信する場合は、HTMLのmetaタグとしてCSRFトークンの情報を出力し、metaタグから取得したトークン値をAjax通信時のリクエストヘッダに設定して連携する。
まず、Spring Securityから提供されているJSPタグライブラリを使用して、HTMLのmetaタグにCSRFトークンの情報を出力する。
- JSPの実装例
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<head>
    <!-- omitted -->
    <sec:csrfMetaTags /> <!-- (1) -->
    <!-- omitted -->
</head>
| 項番 | 説明 | 
|---|---|
| (1) | HTMLの <head>要素内に<sec:csrfMetaTags>要素を指定する。 | 
<sec:csrfMetaTags>要素を指定すると、以下のような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のトークンチェックエラー時に発生する例外は以下の通りである。
| クラス名 | 説明 | 
|---|---|
| InvalidCsrfTokenException | クライアントから送られたトークン値と、サーバー側で保持しているトークン値が一致しない場合に使用する例外クラス(主に不正なリクエスト)。 | 
| MissingCsrfTokenException | サーバー側にトークン値が保存されていない場合に使用する例外クラス(主にセッション切れ)。 | 
DelegatingAccessDeniedHandlerクラスを使用して上記の例外をハンドリングし、それぞれに AccessDeniedHandlerインタフェースの実装クラスを割り当てることで、例外毎の遷移先を設定することが可能である。
CSRFトークンチェックエラー時に専用のエラー画面(JSP)に遷移させたい場合は、以下のような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="/WEB-INF/views/common/error/invalidCsrfTokenError.jsp" />
                </bean>
            </entry>
            <!-- (5) -->
            <entry
                key="org.springframework.security.web.csrf.MissingCsrfTokenException">
                <bean
                    class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                    <property name="errorPage"
                        value="/WEB-INF/views/common/error/missingCsrfTokenError.jsp" />
                </bean>
            </entry>
        </map>
    </constructor-arg>
    <!-- (6) -->
    <constructor-arg index="1">
        <bean
            class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
            <property name="errorPage"
                value="/WEB-INF/views/common/error/accessDeniedError.jsp" />
        </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) | keyにAccessDeniedExceptionのサブクラスを指定する。valueとして、AccessDeniedHandlerの実装クラスである、org.springframework.security.web.access.AccessDeniedHandlerImplを指定する。propertyのnameにerrorPageを指定し、valueに表示するviewを指定する。マッピングするExceptionに関しては、トークンチェックエラー時の遷移先の制御 を参照されたい。 | 
| (5) | (4)のExceptionと異なるExceptionを制御したい場合に定義する。 本例では  InvalidCsrfTokenException、MissingCsrfTokenExceptionそれぞれに異なる遷移先を設定している。 | 
| (6) | コンストラクタの第2引数で、デフォルト例外((4)(5)で指定していない  AccessDeniedExceptionのサブクラス)時の例外ハンドラ(AccessDeniedHandler実装クラス )と遷移先を指定する。 | 
Note
無効なセッションを使ったリクエストの検知
セッション管理機能の「無効なセッションを使ったリクエストの検知」処理を有効にしている場合は、MissingCsrfTokenExceptionに対して「無効なセッションを使ったリクエストの検知」処理と連動するAccessDeniedHandlerインタフェースの実装クラスが適用される。
そのため、MissingCsrfTokenExceptionが発生すると、「無効なセッションを使ったリクエストの検知」処理を有効化する際に指定したパス(invalid-session-url)にリダイレクトする。
Note
ステータスコード403以外を返却したい場合
リクエストに含まれるCSRFトークンが一致しない場合に、ステータスコード403以外を返却したい場合は、org.springframework.security.web.access.AccessDeniedHandlerインタフェースを実装した、独自のAccessDeniedHandlerを作成する必要がある。
