saba1024のブログ

どうでも良い思いついた事とかプログラミング関係のメモとか書いていきます。

Grails3.2でのCSRF対策

CSRF対策を導入

標準だとGSPのフォームで利用する2重投稿防止用の<g:form useToken="true" ...>を使うことでCSRF対策が出来る。
ただしこの場合、当然コントローラ側でトークンが正しいかどうかのチェックをしないといけないので、

withForm {
   // OK!
}.invalidToken {
   // トークンがおかしい!
}

というコードを必要な箇所に全て記述しなければならない。
シンプルなコードとは言え、さすがに全コントローラにこのような記述を毎回記述するのは大変。
ということで、基本的にはSpringSecurityCoreを利用するのが楽。

SpringSecurityCoreでの設定

  • resource.groovyに以下を追記。
csrfFilter(CsrfFilter, new HttpSessionCsrfTokenRepository()) {
    // 振る舞いは自分でクラスを用意することで、以下のようにして変更可能
    // accessDeniedHandler = ref('自作AccessDeniedHandler')
    // requireCsrfProtectionMatcher = ref('自作RequireCsrfProtectionMatcher')
}
  • Bootstrap.groovyに以下を追加
SpringSecurityUtils.clientRegisterFilter('csrfFilter',  SecurityFilterPosition.LAST.order + 10)

基本的な設定はこの2つのみ。

クライアント側の設定

で、基本的なFORMからのPOSTとかの場合は、

<input type="hidden" name="_csrf" value="${_csrf.token}">

を付けるだけでもう完了。
これで後はSpringSecurityCoreが全て全自動でチェックしてくれる。わざわざ自分でコントローラ側にトークンの妥当性チェックを書く必要をない。

ただし、JavaScriptからajaxでデータを投げる場合にはHTTPヘッダーに埋め込む方が良いらしい。
なので、基本的には全ページのheadタグ内に以下の内容を追記。

<meta name="_csrf" content="${_csrf?.token}"/>
<meta name="_csrf_header" content="${_csrf?.headerName}"/>

で、実際にサーバにデータを投げるJavaScriptで、上記の2つのデータを元にHTTPヘッダーを生成すればOK。
以下は、自分が今作っているアプリケーションで、jQueryでmetaタグの情報を取得して、axiosで実際のそのヘッダーと共にデータをサーバに投げるサンプル。

// metaタグから値を取得
const csrfHeader = $("meta[name='_csrf_header']").attr("content");
const csrfToken = $("meta[name='_csrf']").attr("content");
const headers = {};
headers[csrfHeader] = csrfToken;

axios.post(
    '/u/r/l',  {
        // サーバに投げるデータを指定
        name: 'grails'
        version: '3.2.9'
    }, {
        // コレがそのヘッダー
        headers: headers
    }
    )
.then(function (response) {
    console.log(response);
})
.catch(function (error) {
    console.log(error);
});

(ちなみにヘッダー名はデフォルトではX-CSRF-TOKEN

なお、Bootstrapに追加したコードの詳細は現在調査中。
もしこれを省いた場合、別のサイトから未ログイン状態のユーザが悪意を持ってPOSTとかPUT、DELETEで遷移させられてきた場合には、まずログイン画面が表示される。
ユーザがもし疑問に思わずにログインすると、GET扱いでログイン先のページに移ってしまう。 (GETになるのでPOSTとかで送られたパラメタはnullになる為、大抵データを保存するような場面なのでvalidationエラーになるはず。だが、それは本来意図したエラーチェックではない)

上記のBootstrapの1行を追加すればちゃんとSpringSecurityCoreがエラー扱いにしてくれる(Statusコードが403じゃなくて999になるので本当に意図したエラーなのか不安。。。ここの辺りを現在調査中)

あと、Bootstrapの1行を追加するとログインフォームにもTokenが必要になるので、views/login/auth.gpsとdenied.gspプラグインからコピーしてきて修正する。

参考ページ

17 Security
8.1.11 Handling Duplicate Form Submissions
Grails 3 CSRF protection
6.7.2.3. AjaxによるCSRFトークンの送信
Spring Security Core Plugin - Reference Documentation