Table Of Contents

Previous topic

6.6. XSS対策

Next topic

7. Appendix

This Page

Caution

本バージョンの内容は既に古くなっています。最新のガイドラインはこちらからご参照ください。

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対策機能は、Spring Security3.2から提供される機能であるが、共通ライブラリ(terasoluna-gfw-security-web)の1.0.0.RELEASE版が依存している Spring Securityのバージョンは、3.1.4.RELEASEである(共通ライブラリの1.0.0.RELEASE版リリース時には、Spring Securityの3.2.0.RELEASE版は未リリースであるため)。 このため、terasoluna-gfw-security-webプロジェクト内に、1.0.0.RELEASE版リリース時のSprinng SecurityのCSRF対策機能に関する以下のクラスが同梱されている。

  • org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy
  • org.springframework.security.web.csrf.CsrfAuthenticationStrategy
  • org.springframework.security.web.csrf.CsrfFilter
  • org.springframework.security.web.csrf.CsrfLogoutHandler
  • org.springframework.security.web.csrf.CsrfToken
  • org.springframework.security.web.csrf.CsrfTokenRepository
  • org.springframework.security.web.csrf.DefaultCsrfToken
  • org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository
  • org.springframework.security.web.csrf.InvalidCsrfTokenException
  • org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor

共通ライブラリのバージョンアップのタイミングで、Spring Securityのバージョンアップし、上記のクラスは、terasoluna-gfw-security-webプロジェクトからは取り除かれる予定である。


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:custom-filter ref="csrfFilter" before="LOGOUT_FILTER" />  <!-- (1) -->

     <sec:session-management
         session-authentication-strategy-ref="sessionAuthenticationStrategy" />  <!-- (2) -->
     <!-- omitted -->
 </sec:http>

 <bean id="csrfFilter" class="org.springframework.security.web.csrf.CsrfFilter">  <!-- (3) -->
     <constructor-arg index="0" ref="csrfTokenRepository" />  <!-- (4) -->
     <property name="accessDeniedHandler">
         <bean
             class="org.springframework.security.web.access.AccessDeniedHandlerImpl">  <!-- (5) -->
             <property name="errorPage"
                 value="/WEB-INF/views/common/error/csrfTokenError.jsp" />  <!-- (6) -->
         </bean>
     </property>
 </bean>

 <bean id="csrfTokenRepository"
     class="org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository" />  <!-- (7) -->

 <bean id="sessionAuthenticationStrategy"
     class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy"> <!-- (8) -->
     <constructor-arg index="0">
         <list>
             <!-- omitted -->
             <bean
                 class="org.springframework.security.web.csrf.CsrfAuthenticationStrategy">  <!-- (9) -->
                 <constructor-arg index="0"
                     ref="csrfTokenRepository" />  <!-- (10) -->
             </bean>
         </list>
     </constructor-arg>
 </bean>
項番 説明
(1)
<sec:custom-filter>要素を定義し、org.springframework.security.web.authentication.logout.LogoutFilterの前に CSRFのFilter定義を行う。
(2)
<sec:session-management>要素の、session-authentication-strategy-ref属性で、
org.springframework.security.web.authentication.session.SessionAuthenticationStrategyを参照する。
(3)
org.springframework.security.web.csrf.CsrfFilterのbean定義を行う。
(4)
コンストラクタの第1引数で、トークンの作成、保持を行うorg.springframework.security.web.csrf.CsrfTokenRepositoryを参照する。
(5)
accessDeniedHandlerプロパティにorg.springframework.security.web.access.AccessDeniedHandlerImplを bean定義する。
(6)
AccessDeniedHandlerImplerrorPageプロパティに、リクエストに含まれるCSRFトークンが、一致しない場合の遷移先パスを設定する。
設定を省略した場合、リクエストに含まれるCSRFトークンが一致しない場合、ステータスコード403でクライアントに返却する。
(7)
CsrfTokenRepositoryの実装としてHTTPセッションにCSRFトークンを保存する、org.springframework.security.web.csrf.HttpSessionCsrfTokenRepositoryクラスを定義する。
(8)
SessionAuthenticationStrategyの実装として、複数のSessionAuthenticationStrategyを使用できるorg.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy
(9)
CompositeSessionAuthenticationStrategyに、org.springframework.security.web.csrf.CsrfAuthenticationStrategyを追加する。
(10)
CompositeSessionAuthenticationStrategyコンストラクタの第1引数で、CsrfTokenRepositoryを参照する。

Note

AccessDeniedHandlerImplのerrorPageプロパティを省略した場合の、エラーハンドリングについて

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

web.xml

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

Note

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

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

Todo

Spring Security のバージョンが、 3.2.0 以降の場合の設定

Spring Security 3.2を使用する場合、<sec:http>要素に<sec:csrf />要素を設定することで、 前述した設定を省略することができる。

Spring Securityのレファレンスドキュメントを参照されたい。

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"
                 factory-method="create" /> <!-- (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定義を設定する。
factory-methodに、createメソッドを指定する。

Note

CSRFトークンの生成、チェックは、CsrfFilterが行うため、Controllerでは特に、CSRF対策は意識しなくてよい。

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

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

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

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

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" />
</form>

自動で、_name属性が_csrfである、<input type="hidden">タグが追加され、CSRFトークンが埋め込まれているとわかる。

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

Warning

CsrfRequestDataValueProcessorの設定をしている場合、すべてのリクエストにCSRFトークンを埋め込むため、<form:form method="GET" ...>...</form:form>のように、 <form:form>でGETを指定してフォームを送信した場合、ブラウザのアドレスバーのURLに、CSRFトークンが含まれ、ブックマークバーやアクセスログに残る。

これを回避するためには、

<form:form method="GET" modelAttribute="xxxForm" action="...">
...
</form:form>

と書く代わりに、

<form method="GET" action="...">
  <spring:nestedPath path="xxxForm">
  ...
  </spring:nestedPath>
</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.

と説明されており、必須ではないが対応することが推奨される。

Spring Securityのデフォルト実装は、CSRFトークンをUUIDとして生成し、Session IDは使用しないため、CSRFトークンが漏洩してもSession IDは漏洩することはない。またログインの度にCSRFトークンは変更される。 また、将来的にはSpring 4でこの問題は解消される(<form:form method="GET">を使用してもCSRFトークンはURLに現れない)。

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

<form:form>タグを使用しない場合は、明示的に、<input type="hidden">タグを追加する必要がある。

CsrfFilterにより、org.springframework.security.web.csrf.CsrfTokenオブジェクトが、リクエストスコープの _csrf属性に設定されるため、jspでは、以下のように設定すればよい

<form method="POST"
  action="${pageContext.request.contextPath}/csrfTokenCheckExample">
    <input type="submit" name="second" value="second" />
    <input type="hidden" name="${f:h(_csrf.parameterName)}" value="${f:h(_csrf.token)}"/>  <!-- (1) -->
</form>
項番 説明
(1)
_csrf.parameterNameでリクエストパラメータ名を、_csrf.tokenで、CSRFトークンを設定する。

以下のような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>

Note

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

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

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

Note

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

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

jspの実装例

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

 $(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>'); // (6)
         }).fail(function(data) {
             // error handling
             alert(data.statusText);
         });
     });
 });
 </script>
項番 説明
(1)
<meta>タグに、${f:h(_csrf.token)}で取得したCSRFトークンを設定する。
(2)
<meta>タグに、${f:h(_csrf.headerName)}で取得したヘッダ名を設定する。
(3)
<meta>タグに、設定したCSRFトークンを取得する。
(4)
<meta>タグに、設定したCSRFヘッダ名を取得する。
(5)
リクエストヘッダーに、<meta>タグで設定したヘッダ名(デフォルト:X-CSRF-TOKEN)、CSRFトークンの値を設定する。
(6)
この書き方はXSSの可能性があるので、実際にJavaScriptコードを書くときは気を付けること。
今回の例ではdata.addResultdata.subtractResultdata.multipyResultdata.divideResultの全てが数値型であるため、問題ない。

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

Todo

Ajax対応する例がなくなっているため、例を直す。

6.7.2.4. マルチパートリクエスト(ファイルアップロード)時の留意点

一般的に、ファイルアップロードなどマルチパートリクエストを送る場合、formから送信される値をFilterでは取得できない。
そのため、これまでの説明だけでは、マルチパートリクエスト時にCsrfFileterがCSRFトークンを取得できず、不正なリクエストと見なされてしまう。

そのため、以下のどちらかの方法によって、対策する必要がある。

  • org.springframework.web.multipart.support.MultipartFilterを使用する
  • クエリのパラメータでCSRFトークンを送信する

6.7.2.4.1. MultipartFilterを使用する方法

通常、マルチパートリクエストの場合、formから送信された値はFilter内で取得できない。
org.springframework.web.multipart.support.MultipartFilterを使用することで、マルチパートリクエストでも、Filter内で、
formから送信された値を取得することができる。

MultipartFilterを使用するには、以下のように設定すればよい。

web.xmlの設定例

<filter>
    <filter-name>MultipartFilter</filter-name>
    <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class> <!-- (1) -->
</filter>
<filter>
    <filter-name>springSecurityFilterChain</filter-name> <!-- (2) -->
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>MultipartFilter</filter-name>
    <servlet-name>/*</servlet-name>
</filter-mapping>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
項番 説明
(1)
org.springframework.web.multipart.support.MultipartFilterを 定義する。
(2)
springSecurityFilterChainより前に、MultipartFilterを定義すること。

JSPの実装例

<form:form action="${pageContext.request.contextPath}/fileupload"
    method="post" modelAttribute="fileUploadForm" enctype="multipart/form-data">  <!-- (1) -->
    <table>
        <tr>
            <td width="65%"><form:input type="file" path="uploadFile" /></td>
        </tr>
        <tr>
            <td><input type="submit" value="Upload" /></td>
        </tr>
    </table>
</form:form>
項番 説明
(1)
spring-mvc.xmlの設定の通り、CsrfRequestDataValueProcessorが定義されている場合、
<form:form>タグを使うことで、CSRFトークンが埋め込まれた<input type="hidden">タグが自動的に追加されるため、
JSPの実装で、CSRFトークンを意識する必要はない。

<form> タグを使用する場合
CSRFトークンを明示的に埋め込む方法でCSRFトークンを設定すること。

6.7.2.4.2. クエリパラメータでCSRFトークンを送る方法

JSPの実装例

<form:form action="${pageContext.request.contextPath}/fileupload?${f:h(_csrf.parameterName)}=${f:h(_csrf.token)}"
    method="post" modelAttribute="fileUploadForm" enctype="multipart/form-data"> <!-- (1) -->
    <table>
        <tr>
            <td width="65%"><form:input type="file" path="uploadFile" /></td>
        </tr>
        <tr>
            <td><input type="submit" value="Upload" /></td>
        </tr>
    </table>
</form:form>
項番 説明
(1)
<form:form>タグのaction属性に、以下のクエリを付与する必要がある。
?${f:h(_csrf.parameterName)}=${f:h(_csrf.token)}
<form>タグを使用する場合も、同様の設定が必要である。
ファイルアップロードの詳細については、FileUploadを参照されたい。

Warning

この方法も前述したCSRFがURLに現れるという問題がある。 MultipartFilterを使用する場合はspringSecurityFilterChainによる処理がアップロードの後になるため、 認証されていないユーザーのアップロード(一時ファイル作成)を許容してしまう。これを防ぐ必要がある場合に、クエリパラメータでCSRFトークンを送る必要がある。