Rails 4のturbolinksについて最低でも知っておきたい事
(追記)turbolinksに関するセキュリティ上の懸念について
turbolinksとは、ページ遷移をAjaxに置き換え、JavaScriptやCSSのパースを省略することで高速化するgemで、Rails 4からはデフォルトで使用されるようになります。
高速化は大歓迎なのですが、JavaScriptのイベントの起き方が変わるため、Rails 3までの書き方をしているとまず間違いなく問題が起きます。しかも、Rails 4ではデフォルトの機能ですので、最新版を使いたいなら必ず知っておかなければいけません。
本エントリではturbolinksを使うために絶対に知らなければいけないことを分かりやすく紹介したいと思います。
動作
turbolinksの動作は、すごく大雑把に言うと以下の通りです。
- リンクのclickイベントをフック
- リンク先のページをXHRで取り寄せる
- レスポンスをDOM化
- 現在のページと取り寄せたページの外部JavaScriptファイルとCSSファイルが同一なら、titleとbodyを読み込んだページのもので置き換える
実際に上の動作になるのはGETの場合だけで、無効になる条件や方法もあります(後述)。また、前のページの内容は最大10ページ分キャッシュに保存されます。
イベント
turbolinksでページをロードすると、jQueryのreadyイベントが発火しません。$(function(){})に入ってる処理が実行されないということです。代わりに以下の4つのイベントが用意されています。
- page:fetch XHRを発行する前
- page:load XHRでページが更新された
- page:restore キャッシュからページが復元された
- page:change 何らかの方法でページが更新された
XHRが使われた場合、イベントの順序は
- page:fetch
- page:change
- page:load
キャッシュから復元された場合は
- page:change
- page:restore
となります。なお、イベントのtargetはdocumentです。
よくある問題
readyが発火しない
前述の通り、まずreadyイベントハンドラが呼ばれないという問題が起きます。readyとpage:loadを使い分けるのが正しい対処ですが、page:loadのタイミングでreadyイベントハンドラを実行してくれるjquery.turbolinksというgemもあります。
ではjquery.turbolinksを導入すればturbolinks対応完了かというと、そうは行かず、jquery.turbolinksのREADME.mdにも書かれている通り、documentに登録したイベントハンドラが複数回実行されるようになることがあります。これは例えば、以下のようなコードを書いている場合に発生します。
[cc lang="javascript"]
$(function(){
$(document).on('click', '.mySelector', myHandler);
});
[/cc]
上のコードでは、readyイベントでdocumentにイベントハンドラを登録しています。ところがturbolinksによってページが更新された場合、jquery.turbolinksによってreadyイベントハンドラは実行されますがdocumentオブジェクトは前のページから引き継がれるため、同じdocumentオブジェクトに2つ目のイベントハンドラが登録されてしまいます。
これを避けるには、以下のようにdocumentへのイベントハンドラ登録を$(function(){})の外でやるようにします。
[cc lang="javascript"]
$(function(){});
$(document).on('click', '.mySelector', myHandler);
[/cc]
windowのloadイベントが発火しない
前述のreadyイベントはページ中の全てのDOMが参照可能になったときに発火しますが、windowのloadイベントはページ中の全ての要素をロードし終わった時に発火します。例えばJavaScriptコードの中で画像のサイズを使用する場合など、loadイベントが必要になることがありますよね。ところがturbolinksでページが更新されたときはwindowのloadイベントハンドラも実行されません。
この問題を回避する最も簡単な方法は、当該ページへの遷移時にturbolinksを無効にすることです。turbolinksを無効にするには、リンクかその先祖要素にdata-no-turbolink属性を付けます。例えば以下のように書いてあると、/fooへ遷移するときturbolinksは動作しません。
[cc lang="html"]
[/cc]
metaタグが更新されない
turbolinksが更新してくれるのはtitleとbodyだけですので、head内にあるmetaは対象外です。これは例えば、GETでログアウトできるようなサイトで問題になります。問題発生のシナリオは以下の通りです。
- ユーザがログインフォームを開き、csrf-tokenの入ったmetaタグが表示される
- ログインする
- GETでログアウトする。session中のcsrf-tokenは更新されるが、metaタグは更新されない
- 再びログインフォームを開く。ujsによってmetaタグのcsrf-tokenがフォームにコピーされる
- sessionとフォームのcsrf-tokenが異なるためInvalidAuthenticityTokenが発生
この問題はログアウトをDELETEで行なっていれば発生しませんが、GETのログアウトが必要な事情もあるでしょう。このような場合には、bodyの中にmetaを更新するスクリプトを書くことで回避できます。しかし、あまり行儀の良いやり方とは言えないでしょう。turbolinksにhead内のタグを更新させるpull requestもありますが、DHHは「これが不可欠なユースケースを聞いたことがない」と一蹴しています。
まとめ
turbolinksを使う際には以下のことに注意する必要があります。
GETでページ遷移したとき:
- jQueryのreadyイベントは起きない
- windowのloadイベントも起きない
- metaタグが更新されない
面倒かもしれませんがウェブアプリの進化は待ってくれません。では、いつ使うか?今でしょ!
(追記)turbolinksに関するセキュリティ上の懸念について
turbolinksを採用しているサイトでは、条件次第で攻撃者が任意のJavaScriptを実行させられます(いわゆるXSS)。攻撃が成立するパターンには以下の2つがあります。
パターン1:自動リンクとアップロード
条件
- ユーザが投稿したテキストにURLが入っていると自動的にリンクになって表示されること
- ファイルをアップロードでき、同じサイトのURLが付与されてダウンロード可能になること
攻撃成立の流れは以下の通りです。
- 攻撃用スクリプトを含んだHTMLファイルを、拡張子を偽ってアップロードする。攻撃用ファイルはURLが付けられ、ダウンロード可能になる。なお、攻撃用ファイルはturbolinksが効くようにheadの中身を書く
- 攻撃用ファイルのURLを投稿する。このときファイル名の後に「#foo.html」などを加え、リンクの最後をhtmlにする。自動的にリンクとなって表示される
- 被害者がリンクを辿る。turbolinksによって攻撃用ファイルがロードされ、スクリプトが実行される
対策はいくつかありますが、例えば自動リンクにはdata-no-turbolinkをつけ、アップロードされたファイルがContent-Disposition: attachmentで配信されるようにします。
パターン2:自動リンクとオープンリダイレクタとXHR Level 2
条件
- ユーザが投稿したテキストにURLが入っていると自動的にリンクになって表示されること(パターン1と同じ)
- パラメータとしてURLを渡すとそこへリダイレクトする機能がサイト内にあること
- XHRで外部サイトへリダイレクトするブラウザ(IE10など)を被害者が使っていること
こちらは以下のような流れで攻撃が成立します。
- 攻撃者は攻撃用スクリプトを含んだページを自分のサイトに置く。攻撃用ページのheadはturbolinksが効くように書く
- 攻撃先サイトのオープンリダイレクタを通して攻撃用ページへ行くURLを投稿する。自動的にリンクとなって表示される
- 被害者がリンクを辿る。turbolinksが攻撃用ページをリダイレクトで取得し、中のスクリプトを実行する
こちらも自動リンクにdata-no-turbolinkを付けることが対策になります。
関連情報
- Potential XSS attack with Turbolinks · Issue #195 · rails/turbolinks
- Rails4 の Turbolinks について最低限知っておきたいこととその他 - HeartRails Tech Blog
- ssig33.com - turbolinks で実際に攻撃する為に必要な事
(追記ここまで)
このエントリーに対するコメント
- トラックバック
-
- pushStateでURLを書き換える » RainbowDevilsLand2013/07/26, 12:36 AM
[…] 詳しくはこちら。 Rails 4のturbolinksについて最低でも知っておきたい事 […]
-
- 2013年Rubyの話題を一挙に振り返るまとめ | Engine Yard Blog JP2013/12/29, 3:36 PM
[…] Rails 4のturbolinksについて最低でも知っておきたい事 | KRAY Inc […]
「いいね!」で応援よろしくお願いします!