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" /> <!-- (1) -->
</form>
項番 説明
(1)
Spring Securityのデフォルト実装では、name属性に_csrfが設定されている <input type="hidden">タグが追加され、CSRFトークンが埋め込まれる。

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

Warning

CsrfRequestDataValueProcessorを設定している状態で <form:form method="GET" ...>...</form:form>とした場合(GETメソッドを指定してフォームを送信した場合)、

  • ブラウザのアドレスバーにCSRFトークンが表示される
  • ブックマークした場合、ブックマークにCSRFトークンが記録される
  • WebサーバのアクセスログにCSRFトークンが記録される

ため、攻撃者に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を生成しているため、 仮に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>

以下のような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.parameterNameでリクエストパラメータ名を、_csrf.tokenで、CSRFトークンを設定する。
(2)
Spring Securityのデフォルト実装では、name属性に_csrfが設定されている <input type="hidden">タグが追加され、CSRFトークンが埋め込まれる。

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トークンを送信する

Note

それぞれメリット・デメリットが存在するため、システム要件を考慮して、採用する対策方法を決めて頂きたい。

ファイルアップロードの詳細については、FileUploadを参照されたい。

6.7.2.4.1. MultipartFilterを使用する方法

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

Warning

MultipartFilterを使用した場合、springSecurityFilterChainによる認証・認可処理が行われる前にアップロード処理が行われるため、 認証又は認可されていないユーザーからのアップロード(一時ファイル作成)を許容してしまう。

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トークンを送る方法

認証又は認可されていないユーザーからのアップロード(一時ファイル作成)を防ぎたい場合は、 MultipartFilterは使用せず、クエリパラメータでCSRFトークンを送る必要がある。

Warning

この方法でCSRFトークンを送った場合、<form:form method="GET" ...>...</form:form>使用時と同様に、CSRFトークンがURLに現れるという問題がある。

以下に、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>タグを使用する場合も、同様の設定が必要である。