6.7. CSRF対策

6.7.1. Overview

Cross site request forgeries(以下、CSRFと略す)とは、Webサイトにスクリプトや自動転送(HTTPリダイレクト)を実装することにより、
ユーザが、ログイン済みの別のWebサイト上で、意図しない何らかの操作を行わせる攻撃手法のことである。
サーバ側でCSRFを防ぐには、以下の方法が知られている。
  • 秘密情報(トークン)の埋め込み
  • パスワードの再入力
  • Referのチェック
OWASPでは、トークンパターンを使用する方法が推奨されている。
csrf check other site

Picture - csrf check other site

Note

OWASPとは

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

CSRFを回避する方法は、前述したように複数あるが、固定トークンを使用するライブラリを、Spring Securityが提供している。
セッション毎に1つの固定トークンを用い、すべてのリクエストについて、同じ値を使用している。
デフォルトではHTTPメソッドが、GET,HEAD,TRACE,OPTIONS以外の場合、
リクエストに含まれるCSRFトークンをチェックし、値が一致しない場合は、エラー(HTTP Status:403[Forbidden])とする。
csrf check other kind

Picture - csrf check other kind

Tip

CSRFトークンチェックは、別サイトからの不正な更新リクエストをチェックし、エラーとするものである。 ユーザに順序性(一連の業務フロー)を守らせ、チェックするためには、トランザクショントークンチェックについてを参照されたい。

Warning

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

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


6.7.2. How to use

6.7.2.1. Spring Securityの設定

Spring SecurityのCSRF機能を使用するための設定を説明する。 Spring Security の How to useで設定したweb.xmlを前提とする。

6.7.2.1.1. spring-security.xmlの設定

追加で設定が必要な箇所を、ハイライトしている。

 <sec:http auto-config="true" use-expressions="true" >
     <!-- omitted -->
     <sec:csrf />  <!-- (1) -->
     <sec:access-denied-handler ref="accessDeniedHandler"/>  <!-- (2) -->
     <!-- omitted -->
 </sec:http>

 <bean id="accessDeniedHandler"
     class="org.springframework.security.web.access.DelegatingAccessDeniedHandler">  <!-- (3) -->
     <constructor-arg index="0">  <!-- (4) -->
         <map>
             <entry
                 key="org.springframework.security.web.csrf.InvalidCsrfTokenException">  <!-- (5) -->
                 <bean
                     class="org.springframework.security.web.access.AccessDeniedHandlerImpl">  <!-- (5) -->
                     <property name="errorPage"
                         value="/WEB-INF/views/common/error/invalidCsrfTokenError.jsp" />  <!-- (5) -->
                 </bean>
             </entry>
             <entry
                 key="org.springframework.security.web.csrf.MissingCsrfTokenException">  <!-- (6) -->
                 <bean
                     class="org.springframework.security.web.access.AccessDeniedHandlerImpl">  <!-- (6) -->
                     <property name="errorPage"
                         value="/WEB-INF/views/common/error/missingCsrfTokenError.jsp" />  <!-- (6) -->
                 </bean>
             </entry>
         </map>
     </constructor-arg>
     <constructor-arg index="1">  <!-- (7) -->
         <bean
             class="org.springframework.security.web.access.AccessDeniedHandlerImpl">  <!-- (8) -->
             <property name="errorPage"
                 value="/WEB-INF/views/common/error/accessDeniedError.jsp" />  <!-- (8) -->
         </bean>
     </constructor-arg>
 </bean>
項番 説明
(1)
<sec:http>要素に<sec:csrf>要素を定義することで、Spring Security のCSRFトークンチェック機能を利用できるようになる。
デフォルトでチェックされるHTTPメソッドについては、こちらを参照されたい。
詳細については、Spring Securityのレファレンスドキュメントを参照されたい。
(2)
AccessDeniedExceptionを継承したExceptionが発生した場合、Exceptionの種類毎に表示するviewを切り替えるためにHandlerを定義する。
全て同じ画面で良い場合は error-page 属性に遷移先のjspを指定することで可能となる。
Spring Securityの機能でハンドリングしない場合は、こちらを参照されたい。
(3)
エラーページを切り替えるためにSpring Securityで用意されているHandlerのclassに org.springframework.security.web.access.DelegatingAccessDeniedHandlerを指定する。
(4)
コンストラクタの第1引数でデフォルト以外のException(AccessDeniedExceptionを継承したException)の種類毎に表示を変更する画面をMap形式で設定する。
(5)
keyに AccessDeniedExceptionを継承したException を指定する。
実装クラスとして、Spring Securityで用意されている org.springframework.security.web.access.AccessDeniedHandlerImpl を指定する。
propertyのnameにerrorPageを指定し、valueに表示するviewを指定する。
(6)
(5)とExceptionの種類が違う場合に表示の変更を定義する。
(7)
コンストラクタの第2引数でデフォルト(AccessDeniedExceptionとコンストラクタの第1引数で指定していないAccessDeniedExceptionを継承したException)の場合のviewを指定する。
(8)
実装クラスとして、Spring Securityで用意されている org.springframework.security.web.access.AccessDeniedHandlerImpl を指定する。
propertyのnameにerrorPageを指定し、valueに表示するviewを指定する。

AccessDeniedExceptionを継承したCSRF対策により発生するExceptionの種類
Exception 発生理由
org.springframework.security.web.csrf.
InvalidCsrfTokenException
クライアントからリクエストしたCSRFトークンとサーバで保持しているCSRFトークンが一致しない場合に発生する。
org.springframework.security.web.csrf.
MissingCsrfTokenException
CSRFトークンがサーバに存在しない場合に発生する。
デフォルトの設定ではCSRFトークンをHTTPセッションに保持するため、CSRFトークンが存在しないということはHTTPセッションが破棄された(セッションタイムアウトが発生した)ことを意味する。

<sec:csrf>要素の token-repository-ref属性でCSRFトークンの保存先をキャッシュサーバやDBなどに変更した場合は、CSRFトークンを保存先から削除した場合にMissingCsrfTokenExceptionが発生する。
これは、トークンの保存先をHTTPセッションにしていない場合は、本機能を使ってセッションタイムアウトの検知が出来ない事を意味している。

Note

CSRFトークンの保存先としてHTTPセッションを使用する場合は、 CSRFトークンのチェック対象のリクエストに対してセッションタイムアウトを検出することができる。

セッションタイムアウト検知後の動作は、<session-management>要素のinvalid-session-url属性の指定によって異なる。

  • invalid-session-url属性の指定がある場合は、セッションを生成した後にinvalid-session-urlに指定したパスへリダイレクトされる。
  • invalid-session-url属性の指定がない場合は、<access-denied-handler>要素に指定したorg.springframework.security.web.access.AccessDeniedHandlerの定義に従ったハンドリングが行われる。

CSRFトークンのチェック対象外のリクエストに対してセッションタイムアウトを検出する必要がある場合は、 <session-management>要素のinvalid-session-url属性を指定して検出すればよい。 詳細は、「セッションタイムアウトの検出」を参照されたい。


Note

<sec:access-denied-handler>の設定を省略した場合のエラーハンドリングについて

web.xmlに以下の設定を行うことで、任意のページに遷移させることができる。

web.xml

<error-page>
    <error-code>403</error-code>  <!-- (1) -->
    <location>/WEB-INF/views/common/error/accessDeniedError.jsp</location>  <!-- (2) -->
</error-page>
項番 説明
(1)
error-code要素に、ステータスコード403を設定する。
(2)
location要素に、遷移先のパスを設定する。

Note

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

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

6.7.2.1.2. spring-mvc.xmlの設定

CSRFトークン用のRequestDataValueProcessor実装クラスを利用し、Springのタグライブラリの<form:form>タグを使うことで、自動的にCSRFトークンを、hiddenに埋め込むことができる。

 <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.terasoluna.gfw.web.mvc.support.RequestDataValueProcessorを複数定義可能な
org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessorをbean定義する。
(2)
コンストラクタの第1引数に、org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessorのbean定義を設定する。

Note

CSRFトークンの生成及びチェックは <sec:csrf />の設定で有効になる CsrfFilterにより行われるので、開発者はControllerで特にCSRF対策は意識しなくてよい。

6.7.2.2. フォームによるCSRFトークンの送信

JSPで、フォームからCSRFトークンを送信するには

  • <form:form>タグを使用してCSRFトークンが埋め込まれた<input type="hidden">タグを自動的に追加する
  • <sec:csrfInput/>タグを使用してCSRFトークンが埋め込まれた<input type="hidden">タグを明示的に追加する

のどちらかを行う必要がある。

6.7.2.2.1. CSRFトークンを自動で埋め込む方法

spring-mvc.xmlの設定の通り、CsrfRequestDataValueProcessorが定義されている場合、 <form:form>タグを使うことで、CSRFトークンが埋め込まれた<input type="hidden">タグが、自動的に追加される。

JSPで、CSRFトークンを意識する必要はない。

<form:form method="POST"
  action="${pageContext.request.contextPath}/csrfTokenCheckExample">
  <input type="submit" name="second" value="second" />
</form:form>

以下のようなHTMLが、出力される。

<form action="/terasoluna/csrfTokenCheckExample" method="POST">
  <input type="submit" name="second" value="second" />
  <input type="hidden" name="_csrf" value="dea86ae8-58ea-4310-bde1-59805352dec7" /> <!-- (1) -->
</form>
項番 説明
(1)
Spring Securityのデフォルト実装では、name属性に_csrfが設定されている <input type="hidden">タグが追加され、CSRFトークンが埋め込まれる。

CSRFトークンはログインのタイミングで生成される。

Tip

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

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

<form:form method="GET" modelAttribute="xxxForm" action="...">
    <%-- ... --%>
</form: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アプリケーション構築の手助けとなる。

6.7.2.2.2. CSRFトークンを明示的に埋め込む方法

<form:form>タグを使用しない場合は、明示的に、<sec:csrfInput/>タグを追加する必要がある。

<sec:csrfInput/>タグを使用すると、CSRFトークンが埋め込まれた<input type="hidden">タグが出力される。

<form method="POST"
  action="${pageContext.request.contextPath}/csrfTokenCheckExample">
    <input type="submit" name="second" value="second" />
    <sec:csrfInput/>  <!-- (1) -->
</form>

以下のようなHTMLが、出力される。

<form action="/terasoluna/csrfTokenCheckExample" method="POST">
  <input type="submit" name="second" value="second" />
  <input type="hidden" name="_csrf" value="dea86ae8-58ea-4310-bde1-59805352dec7"/>  <!-- (2) -->
</form>
項番 説明
(1)
CSRFトークンが埋め込まれた<input type="hidden">タグを出力するために、<sec:csrfInput/>タグを指定する。
(2)
Spring Securityのデフォルト実装では、name属性に_csrfが設定されている <input type="hidden">タグが追加され、CSRFトークンが埋め込まれる。

Note

CSRFトークンチェック対象のリクエスト(デフォルトでは、HTTPメソッドが、GET, HEAD, TRACE, OPTIONS以外の場合)で、CSRFトークンがない、または サーバー上に保存されているトークン値と、送信されたトークン値が異なる場合は、AccessDeniedHandlerによりアクセス拒否処理が行われ、HttpStatusの403が返却される。 spring-security.xmlの設定を記述している場合は、指定したエラーページに遷移する。

6.7.2.3. AjaxによるCSRFトークンの送信

<sec:csrf />の設定で有効になる CsrfFilterは、前述のようにリクエストパラメータからCSRFトークンを取得するだけでなく、
HTTPリクエストヘッダーからもCSRFトークンを取得する。
Ajaxを利用する場合はHTTPヘッダーに、CSRFトークンを設定することを推奨する。JSON形式でリクエストを送る場合にも対応できるためである。

Note

HTTPヘッダ、リクエストパラメータの両方からCSRFトークンが送信する場合は、HTTPヘッダの値が優先される。

Ajaxで使用した例を用いて、説明を行う。追加で設定が必要な箇所を、ハイライトしている。

jspの実装例

 <!-- omitted -->
 <head>
   <sec:csrfMetaTags />  <!-- (1) -->
   <!-- omitted -->
 </head>
 <!-- omitted -->
 <script type="text/javascript">
 var contextPath = "${pageContext.request.contextPath}";
 var token = $("meta[name='_csrf']").attr("content");  <!-- (2) -->
 var header = $("meta[name='_csrf_header']").attr("content");  <!-- (3) -->
 $(document).ajaxSend(function(e, xhr, options) {
     xhr.setRequestHeader(header, token);  <!-- (4) -->
 });

 $(function() {
     $('#calcButton').on('click', function() {
         var $form = $('#calcForm'),
              $result = $('#result');
         $.ajax({
             url : contextPath + '/sample/calc',
             type : 'POST',
             data: $form.serialize(),
         }).done(function(data) {
             $result.html('add: ' + data.addResult + '<br>'
                          + 'subtract: ' + data.subtractResult + '<br>'
                          + 'multipy: ' + data.multipyResult + '<br>'
                          + 'divide: ' + data.divideResult + '<br>'); // (5)
         }).fail(function(data) {
             // error handling
             alert(data.statusText);
         });
     });
 });
 </script>
項番 説明
(1)
<sec:csrfMetaTags />タグを設定することにより、デフォルトでは、以下のmetaタグが出力される。
  • <meta name="_csrf_parameter" content="_csrf" />
  • <meta name="_csrf_header" content="X-CSRF-TOKEN" />
  • <meta name="_csrf" content="dea86ae8-58ea-4310-bde1-59805352dec7" />(content属性の値はランダムなUUIDが設定される)
(2)
<meta name="_csrf">タグに設定されたCSRFトークンを取得する。
(3)
<meta name="_csrf_header">タグに設定されたCSRFヘッダ名を取得する。
(4)
リクエストヘッダーに、<meta>タグから取得したヘッダ名(デフォルト:X-CSRF-TOKEN)、CSRFトークンの値を設定する。
(5)
この書き方はXSSの可能性があるので、実際にJavaScriptコードを書くときは気を付けること。
今回の例ではdata.addResultdata.subtractResultdata.multipyResultdata.divideResultの全てが数値型であるため、問題ない。

JSONでリクエストを送信する場合も、同様にHTTPヘッダを設定すればよい。

Note

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

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