04.CSRF対策
概要
Spring Securityでは、Ver.3.2以降からCSRF対策ができるようになりました。
自作するよりずっと楽です。
ところでセキュリティでよく言われるCSRFという脆弱性はご存知でしょうか?
ご存じの方は以下の記事を読み飛ばしてください。少し長いですので。
【CSRFとは】
この手法では、攻撃者は攻撃対象のサイトを絞っていると考えられます。
攻撃者はたいていの場合、自作のサイトに仕掛けをしておきます。どういった仕掛けかというと、例えば
「攻撃対象のサイトへデータを送信するformのボタン」です。もちろん、ボタンを押したくなるようなコメントを書いておきます。
すると、被害者はこのformのボタンを押して、攻撃対象のサイトへデータを送信してしまうのです。
でも、「ログイン認証しないと操作できないうちのサイトは問題ないでしょ?」って思いますか?
実は、ログイン認証しないと閲覧・操作できないサイトでも被害にあってしまいます。
ログイン認証後に、仕掛けのformのボタンを押させるんです。ログイン後であれば、セッションIDが発行され、
同じブラウザ上のタブではセッションIDが共有されるので、当然、問題なく仕掛けのformからデータ送信され、
攻撃対象のサイトに登録されてしまいます。
攻撃対象が掲示板であれば「爆破予告」などを投稿させるかもしれません。しかもログイン後であれば、被害者の名前で
投稿されます。
これがCSRFによる攻撃の一般的な説明です。
【もう一つの攻撃手法(ログイン画面)】
CSRFに分類されるようですが、もうひとつ、少し毛色の違う攻撃方法があるようです。
それは、まず、攻撃者はあるサイトのログインアカウントを用意します。
そして、ある仕掛けを自作サイトに作ります。
どういった仕掛けかというと、「用意したログインアカウントとパスワードを使ってログインさせるformのボタン」です。
これを被害者が押すと、知らないうちに攻撃者のアカウントで、あるサイトにログインしてしまいます。
そしてその後、被害者がそのサイトを訪れると、気づかないうちに攻撃者のアカウントでアクセスすることになります。
さて、これで何が嬉しいんでしょうか?
実は攻撃者を喜ばすためには、もうひとつ条件があります。
例えば、Googleのようにアクセスしたページ、検索したキーワードを残せるサイトである、などの条件です。
このようなサイトの場合、「被害者が検索したキーワード」という機微の個人情報がログとして残ってしまいます。
攻撃者は自分で用意したものですのでログインID・PWを知っており、ログインして簡単にログを見れてしまいます。
また、住所が登録されたサイトであれば、被害者は住所が間違っていると勘違いし、自分の住所に変更してしまうかもしれません。
そうすると攻撃者に名前と住所が漏えいしてしまいます!
正直、このような状況になる確率は低いと思いますが、サイト運用者としてはこのような脆弱性を残したままにしたくありませんよね。
Spring SecurityによるCSRF対策の基本
一般的なCSRFの対策は、すべてのページにトークンと呼ばれる推測しにくい文字列を埋め込むことです。
トークンはセッションの中に保存し、セッションと結び付けます。
例えば、掲示板の投稿formにトークンを埋め込んで、サーバ側でトークンが正しくなければエラーにして投稿させないようにします。
すると攻撃者が自作サイトに仕掛けたformにはトークンを埋め込めないので、被害者がボタンを押してもエラーになるだけです。
攻撃者がトークンを取得しようとしても、被害者が持っているトークンと違うトークンが発行されるので、問題ありません。
これが一般的なCSRF対策です。
SpringSecurityによるCSRF対策でも同じことをします。
具体的には以下のような動作になっています。
【SpringSecurityによるCSRF対策の動作】
CSRFのトークンを発行し、セッションと結び付けます。
JSPに記述したformにトークンをhiddenで埋め込みます。
ただし、Springの<form:form>タグを使用している場合は、自動的にhiddenのトークンを記述するので、自分で書く必要がありません。
もしSpringの<form:form>タグを使っていない場合でも、明示的にhiddenを記述することができるので問題ありません。
ブラウザからトークンパラメタが送られてくると、SpringSecurityがセッションに保存したトークンと同じかをチェックして、一致しない場合は
403エラーを返却します。もちろん他のステータスを返却することもできます。
ちなみにトークンは一度発行されると基本的にはセッションが破棄されない限り、ずっと同じ値になります。
実際の使用サンプル
<Spring Securityの設定ファイルの記述: /WEB-INF/applicationSecurity.xml>
<sec:http access-denied-page="/error/403.html" path-type="ant" auto-config="false">
・・・
<!-- CSRF対策用のトークンを発行することを宣言します(SprinSecurity3.2以降の機能です) -->
<sec:csrf request-matcher-ref="csrfTarget"/>
</sec:http>
<!--
CSRFトークンチェックを行う画面を指定します。
できれば全てが望ましいが、ここではチェックを行う画面を絞れることを見せるため、
変更や登録の完了画面(finish.html)だけにしました。
すべての画面でトークンのチェックをする場合は以下のbeanは記述の必要がありません。(上のrxxx-refの記述も削除してください)
注意点としてはURLのパス名のルールを決めておかないと以下のようなAntパス形式で指定できないという点です。
【補足】
ログイン画面のCSRF対策はログイン後のコンテンツの内容によってはあまり意味のないものですが、念のためしておいた方がよいと思います。
以下で設定しています(/j_spring_security_check)。
-->
<bean id="csrfTarget" class="org.springframework.security.web.util.matcher.OrRequestMatcher">
<constructor-arg>
<list>
<bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/**/finish.html" />
</bean>
<bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<constructor-arg value="/j_spring_security_check" />
</bean>
</list>
</constructor-arg>
</bean>
CSRF対策をするには、「<sec:csrf />」を記述するだけです。
タグの属性はトークンのチェック画面を指定する場合だけ記述します。
それと、特にチェック画面を指定しなくても、デフォルトではメソッドがGETの場合はチェックがされません。
逆にGETでもチェックさせることもできますが、GETの場合、URLにトークンが見えてしまうので一般的にはあまり好ましくありません。
【CSRFと関係のない補足】
GETのパラメタに盗まれて困る値を載せるのは好ましくない、と一般的には言われます。
GETのパラメタはURLに記述されるので、WEBサーバに到達するまでに通る外部のNW機器などのログなどに残る可能性があるからです。
ただ、httpであるならという条件が付くと思います。
httpsの場合、URLも暗号化されます。ですのでGETのパラメタもPOSTのパラメタも暗号化され、セキュリティ強度は同じになります。
ですのでhttpsであるならば、GETのパラメタにトークンを渡してもそれほどセキュリティ強度が下がるとは言えないのでは?と
個人的には思ってしまいます。
しかし、環境によってはどうなるか分かりませんし、念のためhttpsでもGETのパラメタに知られて困る情報は
載せないようにしています。
<JSPファイルのサンプル: login.jsp>
<form action="<c:url value='j_spring_security_check'/>" method="post">
<table class="login">
<tr>
<th>ログインID</th>
<td><input type="text" name="j_username" value="<c:out value="${SPRING_SECURITY_LAST_USERNAME}"/>"></td>
</tr>
<tr>
<th>ログインPW</th>
<td><input type="text" id="passwd" name="j_password" ></td>
</tr>
</table>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
<input type="submit" name="login" value="ログイン" >
</form>
ここでは、Springの<form:form>タグを使用しない場合のサンプルを記述しました。
<form:form>タグの場合、自動的にトークンのhiddenタグが生成されるので、特にCSRF対策用に記述すべきことはないからです。
さて、<form:form>タグを使用しない場合は、上記のように_csrf.で始まる変数を使用して、nameとvalueを設定します。
それほど難しくないと思います。
Spring設定ファイルへの記述とJSP上に上記の記述1行で、たったのこれだけで対応完了です!
ログアウトの注意点
実は、SpringSecurityのログアウトをPOSTにしなければならないという注意点があります。
GETで記述されていた方は、POSTにするように変更してください。
ログアウトを不正なユーザに行わせないために、ログアウト(/logout)はCSRFトークンのチェックを実施するようになっているようです。
GETはトークンのチェックを行っていません。
もしどうしてもaタグのリンクでログアウトを作成したい場合は、JavascriptでPOSTに変換するなどしてください。
アップロード画面(multipart)でのCSRF対策
本来であれば、厄介なのはアップロード画面でのCSRF対策です。
multipartはPOSTされてくるボディ部のデータの書き方が違うため、パラメタの値を取得できません。
つまり、CSRFトークンの値が取得できずNULLになってしまうため、当然トークンチェックの結果はNGです。
multipartのデータをパースするために、以下のフィルタをSpringSecurityのフィルタの前に置きます。
【web.xmlファイルの記述サンプル】
<!-- マルチパートのフィルター設定です(SpringSecurityの前にこのフィルタを通す)
このフィルタを設定する場合、aplicationContext.xmlにmultipartResolverの設定は不要になります。
-->
<filter>
<filter-name>MultipartFilter</filter-name>
<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>MultipartFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
あと、Spring設定ファイル(applicationContext.xml)に、MultipartResolverをid="filterMultipartResolver"という名前で
設定します。
もしくは上記のフィルタ設定で、init-paramでid名を指定することもできます。
次の記事も参考にしていただければと思います。
動作の確認
設定が完了したら、動作を確認してみてください。
例えば、以下のようにすれば確認できます。
1.まずIEで対策したページを開きます。
2.F12キーを押して開発者ツールを起動します。
3.HTMLソース内を検索して、トークンの記述個所を探します。
4.実は開発者ツールではHTMLのソースを編集できるので、トークン文字列を少し削除して別の値にします。
5.リターンで編集したトークンを確定したら、画面の入力項目を正しく設定してsubmitボタンを押してください。
トークンエラーになるはずです。
補足:トークンの強度
上記CSRFで生成されるトークンの強度が気になる方もいるかと思います。
そこで、トークン生成に使用される乱数生成器が何かを調べてみました。
UUID.randomUUID().toString()
上記のようにrandomUUIDを使用しているようです。
randomUUIDは、SecureRandomを使用しているようで、これは暗号用に強化された乱数生成器だそうです。
どのくらいの強度か、ということまでは調べていませんが、少なくともJavaのRandomよりは強力ということです。
Created Date: 2015/03/21