§クロスサイトリクエストフォージェリ対策
クロスサイトリクエストフォージェリ (CSRF) は、攻撃者が被害者のブラウザに被害者のセッションを使ったリクエストを行わせるように仕向ける、セキュリティ上の脆弱性です。セッショントークンはすべてのリクエストにおいて送信されるので、攻撃者が被害者のブラウザに代わってリクエストを強制できる場合、攻撃者は被害者に代わってリクエストを行えるということになります。
CSRF の攻撃ベクトルがどのようなもので、何が攻撃ベクトルでないかについて習熟することをお勧めします。OWASP のこの情報 から始めるとよいでしょう。
端的に言えば、攻撃者は被害者のブラウザに次の種類のリクエストを行うよう強制することができます:
- あらゆる
GET
リクエスト - ボディタイプが
application/x-www-form-urlencoded
,multipart/form-data
そしてtext/plain
のPOST
リクエスト
攻撃者は次のことは行えません:
- この他の、
PUT
やDELETE
のようなリクエストを使うようブラウザに強制する - この他の、
application/json
のようなコンテントタイプを送信するようブラウザに強制する - サーバが既に設定したものとは異なる、新しいクッキーを送信するようブラウザに強制する
- ブラウザがリクエストに追加する通常のヘッダとは異なる、任意のヘッダを設定するようブラウザに強制する
GET
リクエストは状態を変えないことを意図しているので、このベストプラクティスに従っているアプリケーションに危険はありません。このため、CSRF 対策が必要なのは、上記で言及されたコンテントタイプを持つ POST
リクエストだけです。
§Play の CSRF 対策
Play はリクエストが CSRF リクエストでないことを検証する複数のメソッドを提供しています。その主要なメカニズムは CSRF トークンです。このトークンは、クエリ文字列、または投稿されたすべてのフォームのボディ部分に配置され、同様にユーザーのセッションにも配置されます。そして、Play はこの双方のトークンが存在し、一致することを検証します。
例えば AJAX を通じて行われるような、ブラウザ以外からのリクエストについて簡易に対策できるよう、Play は以下も提供しています:
X-Requested-With
ヘッダが存在する場合、Play はそのリクエストが安全であると見なします。X-Requested-With
は jQuery のようないくつかのポピュラーな Javascript ライブラリがリクエストに追加します。- 値が
nocheck
のCsrf-Token
ヘッダが存在する場合、または適切な CSRF トークンを含む場合、Play はそのリクエストが安全であると見なします。
§グローバル CSRF フィルタの適用
Play はすべてのリクエストに適用できるグローバル CSRF フィルタを提供しています。これがアプリケーションに CSRF 対策を追加するもっとも簡単な方法です。このグローバルフィルタを利用できるようにするには、Play フィルタヘルパーの依存性をプロジェクトの Build.scala
に追加します:
val appDependencies = Seq(
filters
)
ここで Global
オブジェクトにフィルタを追加します:
import play.api._
import play.api.mvc._
import play.filters.csrf._
object Global extends WithFilters(CSRFFilter()) with GlobalSettings {
// ... onStart, onStop etc
}
§現在のトークンを取得する
現在の CSRF トークンには getToken
メソッドを使ってアクセスすることができます。このメソッドは implicit な RequestHeader
を取るので、これがスコープに存在することを確認してください。
import play.filters.csrf.CSRF
val token = CSRF.getToken(request)
フォームへの CSRF トークンの追加を支援するために、Play はいくつかのテンプレートヘルパを提供しています。最初のひとつはトークンをアクション URL のクエリ文字列に追加します:
@import helper._
@form(CSRF(routes.ItemsController.save())) {
...
}
これはフォームを以下のようにレンダリングするでしょう:
<form method="POST" action="/items?csrfToken=1234567890abcdef">
...
</form>
クエリ文字列にトークンを持つことが望しくない場合もあるので、Play は CSRF トークンをフォーム内の hidden フィールドとして追加するヘルパも提供しています:
@form(routes.ItemsController.save()) {
@CSRF.formField
...
}
これはフォームを以下のようにレンダリングするでしょう:
<form method="POST" action="/items">
<input type="hidden" name="csrfToken" value="1234567890abcdef"/>
...
</form>
これらのフォームヘルパーメソッドは、すべてスコープ上で利用できる implicit なトークンまたはリクエストを必要とします。これらは通常、implicit な RequestHeader
パラメータがまだテンプレートに存在しない場合、テンプレートに追加することで提供されます。
§CSRF トークンをセッションに追加する
CSRF トークンがフォーム内にレンダリングされ、そしてクライアントに送り返されることを保証するために、グローバルフィルタは HTML を受け取るすべての GET リクエストにおいて、そのリクエストで既にトークンが利用可能でない場合は、新しいトークンを生成します。
§アクションごとに CSRF フィルタリングを適用する
例えば、アプリケーションがいくつかのサイトをまたがったフォーム送信を許可するようにしたい場合など、グローバル CSRF フィルタが適切でない場合もあるかもしれません。Open ID 2.0 のようなセッションを前提としない標準は、サイトをまたがったフォームの送信、または複数サーバ間の RPC コミュニケーションによるフォーム送信を要求します。
このような状況のために、Play はアプリケーションのアクションに合成できる二つのアクションを提供しています。
最初のひとつは、検査を行う CSRFCheck
アクションです。これは、セッションで認証される POST フォームの投稿を受け取るすべてのアクションに追加する必要があります:
import play.api.mvc._
import play.filters.csrf._
def save = CSRFCheck {
Action { req =>
// handle body
Ok
}
}
二つ目は、入力されたリクエストに既に存在しなければ CSRF トークンを生成する CSRFAddToken
アクションです。これは、フォームをレンダリングするすべてのアクションに追加する必要があります:
import play.api.mvc._
import play.filters.csrf._
def form = CSRFAddToken {
Action { implicit req =>
Ok(views.html.itemsForm())
}
}
これらのアクションをもっと便利に適用する方法は、これらのアクションを Play の ActionBuilder
と組み合わせて使う方法です:
import play.api.mvc._
import play.filters.csrf._
object PostAction extends ActionBuilder[Request] {
def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[SimpleResult]) = {
// authentication code here
block(request)
}
override def composeAction[A](action: Action[A]) = CSRFCheck(action)
}
object GetAction extends ActionBuilder[Request] {
def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[SimpleResult]) = {
// authentication code here
block(request)
}
override def composeAction[A](action: Action[A]) = CSRFAddToken(action)
}
これでアクションを書くために必要なボイラープレートコードを最小化することができます:
def save = PostAction {
// handle body
Ok
}
def form = GetAction { implicit req =>
Ok(views.html.itemsForm())
}
Next: JSON を使う