Lambdaカクテル

京都在住Webエンジニアの日記です

サクサク年表君開発日記(CSRFトークン編)

先日に引き続きサクサク年表君(仮)の開発を進めます。前回はこちら。

blog.3qe.us

Webサービスのセキュリティを支える技術としてCSRFトークン(一般的にはワンタイムトークンと言う?)というものがあります。CSRFを対策するためのトークンです。

Webサービスに負荷やそのデータに修正を加えたりするようなHTTPリクエストを保存し,これをIMGタグやリンクなどを使って送信できるようにし,ユーザが誤ってこれにアクセスすることで自動的に意図しない動作や負荷を与えてしまう攻撃をCSRF(Cross Site Request Forgery,XSRFとも)と呼びます。

www.ipa.go.jp

例えばサクサク年表君に年表を投稿するようなAPIがあり,またそのAPIを叩いて一定の内容を投稿するようなIMGタグやリンクを作成できたとします。これをサクサク年表君のユーザが開いた場合,勝手に自分のアカウントで年表を投稿されてしまいます。大量にばらまくことでサービスに負荷をかけることもできるかもしれません。

CSRFを防ぐためには,高負荷/破壊的なリクエストを受け付ける際,確認画面などを挟みつつあらかじめ事前にユーザにトークンを付与しておき,リクエストとともにトークンを送信させると安全です。こうするとユーザがその動作を確認してからリクエストを送信するため,意図しないリクエストの送信を防ぐことになります。トークンを適宜失効させることで不正に大量のリクエストを送ることも防げます。

定番なのが,HTMLフォームの隠しフィールドやCookieにトークンを付与するという方法です。不正なサイトからリクエストしようとしても,正当なサイトではないため隠しフィールドを経由してトークンを受け取ることができないこと,そして異なるホストから送信されたCookieは別のホストのサイトからは読み取れないことが安全性を担保しています。

www.ipa.go.jp

今回はサクサク年表君がCSRFトークンを使うことができるようにします。

Q. セッションだけでは守れないの?

守れません。ブラウザはアクセス元に関係無く,アクセス先のサイトにそのサイトのCookieを送信します。GETでもPOSTであってもそうです。たまたまサクサク年表君にログインしていた場合,サクサク年表君にはログインセッション(のIDを含んだCookie)が送信されるため,CSRFに脆弱になります。そしてこの挙動はSameSite属性で制御することができますが,ここでは割愛します。

作戦

サクサク年表君のScalaバックエンドはScalatraです。ScalatraにはXsrfTokenSupportというtrait/companion objectとCstfTokenSupportという同様のものがありますが,X-CSRF-TokenX-XSRF-Tokenとは違うらしい。

stackoverflow.com

X-XSRF-Tokenのほうがメジャーなのでこっちにしとけとのこと。難解。HTMLフォームに埋めるだけならどっちでも良さそう。

ひとまずフォームに埋める

AxiosとかのXHRライブラリを使う場合,自動的にCookieにあるX-XSRF-Tokenを見付けて良い感じにパラメータを追加してくれるのですが, フォームに埋めるためには「トークン見付ける」と「トークン埋める」を手動でやる必要があります。

まずはコントローラ側の実装。

class Timeline
    extends ScalatraServlet
    with XsrfTokenSupport { // mix-inするだけで使えるようになる

  /* ...snip... */

  // "/-/edit"エンドポイントはXSRFトークンで守ることを宣言する
  // するとこのエンドポイント突入前にトークンチェックが挟まるようになる
  xsrfGuard("/-/edit")

  // GET側エンドポイント
  // 手でKeyとTokenとをテンプレートエンジンに渡す
  // これらの変数はXsrfTokenSupportが供給する
  get("/-/edit") {
    views.html.edit(xsrfKey, xsrfToken)
  }

  // POST側エンドポイントでは何もする必要なし
  // この中に制御が移った時点でCSRFチェックは済んでいる
  post("/-/edit") {
    "OK!!"
  }
}

views.html.editは以下のような適当なHTMLを置きます。テンプレートエンジンはTwirlです。

@(csrfKey: String, csrfToken: String)
<div class="ui text container">
    <h1>編集</h1>

    <form action="/-/edit" method="POST">
        <input type="hidden" name="@csrfKey" value="@csrfToken" />
        <!-- ↑ここにトークンが入れられる -->
        <button type="submit">go</button>
    </form>
</div>

アクセスしてみよう

f:id:Windymelt:20200628182514p:plain
そうだね

ソースを見てみるとちゃんとトークンが埋まっています。

<input type="hidden" name="org.scalatra.XsrfTokenSupport.key" value="ほげほげ">

submitするとちゃんとOKが返ってきます。

f:id:Windymelt:20200628183110p:plain
物憂げなOKが返ってくる

悪さをしてみます。トークンをいじくってみましょう。

f:id:Windymelt:20200628183223p:plain

してsubmitするとエラー画面に飛ばされます。

f:id:Windymelt:20200628183307p:plain
勝手に用意された画面に飛ばしてくれる

めでたし

まとめ

  • ScalatraでCSRFトークンを使い,CSRF攻撃から守る方法を理解できた。
  • ドキュメンテーションがまったく無かったので結局ソースコードを読む必要があった。
  • HTMLフォームで使う場合はテンプレートエンジンにトークンとキーを手渡さねばならない。
  • AxiosといったライブラリでXHRを行う場合は,自動的にトークンの送信が行われるので特に心配することはない。

そのうちXHRを行うようになるので,XHRする際のCSRFトークンについて,またそのうち解説するかもしれない。