[golang]RevelでCSRFの対策


GoのWeb Framework「Revel」でCSRFの対策をしてみました。
Revelは標準でCSRF対策の仕組みが無いようなので、今回はrevel-csrfというプラグインを利用しました。
CSRFの対策は、後回しにされたり忘れられがちですが、アプリケーションの開発途中で導入するには規模によってはハードルが高くなるため、面倒くさがらずに初期の段階で導入しておきたいです。
revel-csrfの利用方法は、GitHubにも丁寧に記載されてますが記録しておきます。

GitHub
[revel-csrf](https://github.com/cbonello/revel-csrf)

## 前提
* Revelは事前にインストールしてデフォルトページが表示できること
* Revelの基礎知識は持っていること
* ここで説明しているターミナルでのコマンド操作は全てrootで実行しています

## 環境
ホストOS：Mac OSX 10.9.5
ゲストOS：Virtual Box CentOS 7 64bit

Vagrant 1.7.4
Go 1.5
Revel 0.12.0

## revel-csrfをインストール

ターミナルで以下のコマンドを実行すればインストール完了です。

```bash:ターミナル
go get github.com/cbonello/revel-csrf
```
問題なくインストールできれば以下のディレクトリ配下にファイルが保存されます。

```bash:ターミナル
ls -l $GOPATH/src/github.com/cbonello/revel-csrf/
```

## CSRFFilterを仕込む

次にRevelのinit.goにフィルターの設定を追加します。
app/init.goを以下のように編集します。

* github.com/cbonello/revel-csrfをimportに追加する
* revel.Filters[]にcsrf.CSRFFilterを追加する

```go:init.go
package app

import (
    "github.com/cbonello/revel-csrf" //インポートを追加する
    "github.com/revel/revel"
)

func init() {
    // Filters is the default set of global filters.
    revel.Filters = []revel.Filter{
        revel.PanicFilter,             // Recover from panics and display an error page instead.
　　　　　〜 省略 〜
         csrf.CSRFFilter,              // このフィルターを追加する
　　　　　〜 省略 〜
        revel.ActionInvoker,           // Invoke the action.
    }
}
```
あとはHTMLにトークン出力用のhiddenを用意するだけです。

```html:Index.html
<form name="SampleForm">
    <input type="text" name="FormName" />
    <input type="hidden" name="csrf_token" value="{{ .csrf_token }}" />
    <input type="button" value="送信します" />
</form>
```

画面を開いてhiddenタグのvalueを確認すると、ランダムに生成されたトークンの値がセットされているのが確認できます。
またCookieにも同様にトークン（REVEL_SESSION）がセットされています。

![スクリーンショット 2015-12-23 23.45.23.png](https://qiita-image-store.s3.amazonaws.com/0/84601/4ab018fc-6d9e-875e-0351-4cc20b7aac00.png)

あとはトークンがあるFormをPOSTするたびにフィルターが自動でトークンの存在と値の整合性をチェックしてくれます、簡単ですね。
そしてチェックにひっかかると403 Fobiddenがレスポンスで返ってきます。

ちなみにhiddenのnameは"csrf_token"である必要があります。
GitHubに公開されているコード見ると分りますが、デフォルトでは参照する名前がconstで"csrf_token"と定義されているためです。

GitHub
[csrf.go](https://github.com/cbonello/revel-csrf/blob/4dd0f3c795bb32e987fe8eea7f13078581ada38d/csrf.go#L13)

つまりこのあたりを変更したい場合は、インストールしたrevel-csrfのcsrf.goのconstをいじれば任意の名前に変更できます。

```bash:ターミナル
vi $GOPATH/src/github.com/cbonello/revel-csrf/csrf.go
```
当然ですが、上記の名前を変更したらhiddenのnameも合わせて変更します。

## AjaxのCSRF対策

次にAjax通信もCSRF対策します。
というのも、revel-csrfはデフォルトでAjax通信のCSRF対策が無効になっているためです。

GitHubのConfiguration optionsより
> * Revel-csrf supports following configuration options in app.conf:

>csrf.ajax A boolean value that indicates whether or not revel-csrf should support the injection and verification of CSRF tokens for XMLHttpRequests. Default value is false.

要するにXMLHttpRequestsのトークンチェックはデフォルト false だから必要ならapp.confに記載してね、という事らしいです。

というわけで、早速試してみます。

```bash:ターミナル
vi conf/app.conf
```
で、app.confに以下の設定を追記します。

csrf.ajax = true

最初は設定がないと思うので追記します。
追記するさいは[dev]や[prod]など、意図しないセクション内に記述しないよう気をつけて下さい。
無論、限られたセクション内でのみ有効にしたい場合はこの限りではありません。

次にJavaScriptを記述します。
RevelはJQueryが標準でインストールされているので何もせずともJQueryが利用できます。

```js:Index.html
<script>
function csrfSafeMethod(method) {
    // HTTP methods that do not require CSRF protection.
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
    crossDomain: false,
    beforeSend: function(xhr, settings) {
        if (!csrfSafeMethod(settings.type)) {
            xhr.setRequestHeader("X-CSRF-Token", {{ .csrf_token }});
        }
    }
});

$("#AJAXForm").submit(function(event){
    event.preventDefault();
    $.ajax({
        type: "POST",
        url: "/Hello",
        data: {
            name: $("#AJAXFormName").val()
        },
        success: function(data) {
            // Switch to HTML code returned by server on success.
            jQuery("body").html(data);
        },
        error: function(jqXHR, status, errorThrown) {
            alert(jqXHR.statusText);
        },
    });
});
</script>
```

これでAjax通信でPOSTした場合もCSRFのトークンチェックが有効になります。
重要なのはAjax通信でリクエストヘッダーに「X-CSRF-Token」が付与されている事です。
この部分ですね。

```js:Index.html
$.ajaxSetup({
    crossDomain: false,
    beforeSend: function(xhr, settings) {
        if (!csrfSafeMethod(settings.type)) {
            xhr.setRequestHeader("X-CSRF-Token", {{ .csrf_token }});
        }
    }
});
```
Chromeのdeveloper toolでリクエストを見ると確認できます。
![スクリーンショット 2015-12-24 0.34.21.png](https://qiita-image-store.s3.amazonaws.com/0/84601/0fa01193-9e77-a504-cd8b-bf3793cef257.png)

補足
注意点としてGitHubのサンプルコードでは上記のトークン名が"X-CSRFToken"になっていますが正しくは上記の通り"X-CSRF-Token"のようです。
なぜならば先に説明した通り、csrf.goのconstが"X-CSRF-Token"で定義されているためです。

試しにJQueryで”X-CSRF-Token"を"X-CSRFToken"としてAjax通信すると、意図したとりに403エラーが非同期で返ってきます。

こちら確認したらPull Requestされてたので、いずれ修正されると思います。

## さいごに
現状だとページ毎（コントローラー、アクション毎）にトークンチェックを設定することはできなさそうです。
TODOに記載があります。

GitHub TODOより
> * Unique token per-page.

それでもCSRFの対策として十分に機能してくれると思います。
公開されているコードも少ないので、保守やメンテの工数も高くなさそうです。
導入実績の情報は少ないですが、Revelを利用する人は導入を検討してみて下さい。

以上です。