概要
MITライセンスに基いて翻訳・公開いたします。
- リポジトリ: jeremyevans/roda
- 原文更新日: 2018/02/01
- 著者: Jeremy Evans
- サイト: http://roda.jeremyevans.net/
- API: http://roda.jeremyevans.net/rdoc/index.html
roda.jeremyevans.net/より
長いので3本に分割します。
本記事では、原則としてroutesやroutingは「ルーティング」、rootは「ルート」と表記します。
Ruby: 高速/高性能ルーティングエンジンgem「Roda」README: 後編(翻訳)
セキュリティ
Webアプリのセキュリティは非常に広範囲に渡るトピックですが、ここではRodaで実行可能な対策についていくつか説明します。これは、Webアプリでよくある脆弱性の一部を防止するものです。
セッションセキュリティ
セッションを用いる場合、上述のように:secret
オプションでセッションの秘密情報(secret)を必ず設定すべきです。:secret
の値が攻撃者に知られてしまうと、どんなセッション値でも注入できるようになってしまうので、秘密情報は決して漏らさないようにしなければなりません。最悪の場合、リモートコード実行攻撃を受ける可能性があります。
Rack::Session::Cookie
についてぜひ頭の隅に置いていただきたいのは、セッションcookieの内容は暗号化されていないということです。セッションcookieは、改ざんを防ぐために署名されているだけです。つまり、セッションにはいかなる秘密情報も保存してはなりません。
クロスサイトリクエストフォージェリー(CSRF)
Rodaに付属するcsrf
プラグインを用いてCSRFを防止できます。このプラグインはrack_csrfを利用しています。HTMLにCSRFトークンタグが適切な形で含まれていることを確認しておいてください。
Rack::Csrf
ミドルウェアを直接利用することも可能です。この場合csrf
プラグインは不要です。
クロスサイトスクリプティング(XSS)
RodaでXSSを防止する最も容易な方法は、出力をデフォルトで自動エスケープするテンプレートライブラリを用いることです。render
プラグインに:escape
オプションを設定すると、ERBテンプレートプロセッサがデフォルトでエスケープを行うので、出力されるテンプレートは次のようになります。
<%= '<>' %> # outputs <>
<%== '<>' %> # outputs <>
:escape
オプションを使う場合は、コンテンツテンプレートの出力がレイアウトでエスケープされないようにしておく必要があります。
<%== yield %> # <%= yield %>ではなく
このサポートにはErubi gemが必要です。
予想と異なるパラメータ型
Rackは、送信されたパラメータを「文字列」「配列」「ネストしたハッシュ」を含むハッシュに変換します。パラメータの送信はユーザーの制御下にあるので、パラメータの送信については慎重に扱うべきであり、いかなる場合であっても、送信されたパラメータを使う前には必ず明示的にチェックまたは型変換(またはその両方)を実施すべきです。ひとつの方法は、アクセス後に明示的に行うことです。
# foo_idパラメータをIntegerに変換
request.params['foo_id'].to_i
ただし、型変換はうっかり忘れてしまいがちなので、ユーザーがfoo_id
をハッシュまたは配列として送信した場合は、NoMethodError
をraiseするようになっています。型変換を忘れるよりさらにまずいのは、次のように書いてしまうことです。
some_method(request.params['bar'])
some_method
は引数に文字列でもハッシュでも取ることができ、パラメータが文字列として送信されることをあなたが期待しているとすると、some_method
がハッシュ引数を受け取った場合に認証なしで操作が実行されてしまいます。
Rodaには、送信されたパラメータの型キャストを簡単に扱えるtypecast_params
プラグインが付属しています。パラメータを操作するすべてのRodaアプリは、このプラグインを利用するか、送信されたパラメータを別のツールで明示的に期待どおりの型に変換するツールを利用することをおすすめします。
HTTPヘッダーがらみのセキュリティ
以下のHTTPヘッダー設定を詳しく見ておきましょう。これはWebサーバーレベルで設定されますが、default_headers
プラグインを用いてアプリレベルで設定することもできます。
- Content-Security-Policy/X-Content-Security-Policy
- ページでのJavaScriptの扱いやその他のコンテンツタイプの扱いを定義します。
- Frame-Options/X-Frame-Options
- フレーム内部での利用を禁じることで、クリックジャッキングを防止します。
- Strict-Transport-Security
- アプリへのSSL/TLS接続を強制します。
- X-Content-Type-Options
- 一部のブラウザに対して、Content-Typeヘッダーへの配慮を強制します。
- X-XSS-Protection
- 一部のブラウザに対してXSS緩和フィルタを有効にします。
設定例:
class App < Roda
plugin :default_headers,
'Content-Type'=>'text/html',
'Content-Security-Policy'=>"default-src 'self'",
'Strict-Transport-Security'=>'max-age=16070400;',
'X-Frame-Options'=>'deny',
'X-Content-Type-Options'=>'nosniff',
'X-XSS-Protection'=>'1; mode=block'
end
ユーザー入力から派生するテンプレートのレンダリング
Rodaのレンダリングプラグインはデフォルトで、テンプレートがビューディレクトリ内部に配置されていることをチェックします。というのも、ビューディレクトリ外部にあるテンプレートをレンダリングする必要性は通常ないはずであり、よくある攻撃を防ぐからです(特に、ファイルシステム上にユーザーがファイルを書き込める箇所が少しでもある場合に深刻です)。
レンダリングプラグインの:allowed_paths
オプションを用いて、(アクセスを)許可するディレクトリを指定できます。パスチェックをどうしてもオフにしたい場合は、レンダリングプラグインのcheck_paths: false
オプションでできます。このオプションを使うユーザーやライブラリが、こうしたパスを手動でチェックすることを前提にしています。
コードの再読み込み
Rodaには、コード再読み込みをサポートするしくみは統合されていませんが、Rodaアプリと併用できるRackベースのリローダーがあります。
ほとんどのアプリであれば、rack-unreloaderが最も手っ取り早く、安全性もそこそこ高いアプローチでしょう。というのも、変更されたファイルのみを再読込し、再読み込み前にファイルで定義されていた定数をunloadするからです。ただし、アプリのコードの変更にはrack-unreloader固有のAPIを用いることが要求されます。
ファイルの読み込みと定数のunloadを行う類似のソリューションとして、ActiveSupport::Dependencies
というものもあります。ActiveSupport::Dependencies
はアプリのコードの変更が必須ではなく、require
やconst_missing
など一部のコアメソッドを変更します。必要な設定は少なくて済む代わりに、Railsのファイルやクラスの命名規則に依存するので、これに従う必要があります。このライブラリは、存在しない定数にアクセスした場合にファイルを(オンザフライで)自動読み込みする機能も提供しています。アプリが自動読み込みに依存しないのであれば、依存関係のrequireにはrequire_dependency
を使わなければなりません。これ以外の方法では再読み込みされません。
AutoReloaderは、reloadable_paths
オプションのいずれかのエントリから到達するあらゆるファイルについて、透過的な再読み込み機能を提供します。この再読み込みは、トップレベルの定数を検出して、読み込み済みライブラリのうち再読み込み可能なライブラリに変更が生じた場合にこれらの定数を削除することによって行います。このライブラリが有効になると(通常はdevelopment環境で)、require
とrequire_relative
がオーバーライドされます。必須の設定はreloadable_paths
だけです。
rerunとshotgunは、いずれもfork/execアプローチを用いてアプリの新しいバージョンを読み込みます。rerunはアプリに変更が生じた場合に再読み込みを行うだけなのでその分高速であり、shotgunはリクエストのたびにアプリを再読み込みします。いずれもアプリのコードに変更を加えることなく利用できますが、変更のたびにアプリ全体を再読み込みするので遅くなる可能性もあります。しかし、読み込みの速い小規模アプリであればこのどちらかが適していることもあるでしょう。
Rack::ReloaderはRackに付属しており、監視対象ファイルに変更が生じたときに単に再読み込みします(定数はunloadしません)。このライブラリは高速ですが、クラスや定数やメソッドを削除したときや、ファイルの再読み込み時にキャッシュデータを手動できちんとクリアしなかった場合に問題が生じる可能性もあります。
どんなアプリや開発手法にも最適な再読み込みソリューションというものはありません。上述の再読み込みアプローチそれぞれについて必要性やトレードオフを勘案し、最適と思えるものを自分でお選びください。
どれから始めたらよいかわからない場合は、rerunかshotgunから始めるのがよいかもしれません(JRubyやWindows上で実行する場合を除く)。rerunやshotgunの速度が足りない場合にのみ、他のオプションを検討しましょう。
プラグイン
Rodaは設計上、必要不可欠な機能のみを提供する極めて小規模なコアを備えています。本質的でない機能はすべてプラグインという形で追加されます。
Rodaのプラグインは、Rodaのどのメソッドもオーバーライド可能であり、super
でデフォルトの振る舞いを呼び出せます。この設計によって、Rodaの拡張性は非常に高くなっています。
Rodaには、膨大なプラグインやRodaをサポートするその他のライブラリが付属します。
プラグインの作り方
独自プラグインの作成はかなり素直に行なえます。プラグインは単なるモジュールであり、以下のいずれかのモジュールを含んでいます。
InstanceMethods
- Rodaクラスにincludeされるモジュール
ClassMethods
- Rodaクラスをextendするモジュール
RequestMethods
- リクエストのクラスにincludeされるモジュール
RequestClassMethods
- リクエストのクラスをextendするモジュール
ResponseMethods
- レスポンスのクラスにincludeされるモジュール
ResponseClassMethods
- レスポンスのクラスをextendするモジュール
プラグインがload_dependencies
に応答できる場合は、load_dependencies
が最初に呼び出されます。そのプラグインが他のプラグインに依存する場合はload_dependencies
が利用できるようになっているべきです。
プラグインがconfigure
に応答できる場合は、configure
は最後に呼び出されます。プラグインを設定するにはconfigure
が利用できるようになっているべきです。
load_dependencies
やconfigure
はどちらも、プラグイン呼び出しに渡される追加の引数やブロックを伴って呼び出されます。
つまり、インスタンスメソッドを1つ追加するシンプルなプラグインは次のように書けます。
module MarkdownHelper
module InstanceMethods
def markdown(str)
BlueCloth.new(str).to_html
end
end
end
Roda.plugin MarkdownHelper
プラグインを登録する
Rodaプラグインをgemとして同梱したいが、これまでどおりRoda.plugin :plugin_name
から自動的にRodaで読み込みたい場合は、roda/plugins/plugin_name
にプラグインを配置し(ここに置くことでrequireできるようになる)、それからRoda::RodaPlugins.register_plugin
でファイルをプラグインとして登録すべきです。おすすめは、次のようにプラグインモジュールをRoda::RodaPlugins
名前空間に保存することですが、必須ではありません。
class Roda
module RodaPlugins
module Markdown
module InstanceMethods
def markdown(str)
BlueCloth.new(str).to_html
end
end
end
register_plugin :markdown, Markdown
end
end
名前空間を汚したくないのであれば、モジュールをRoda
名前空間に直接作成するのは避けるべきです。また、InstanceMethods
の内部で作成されるインスタンス変数名の冒頭には@_variable
のようにアンダースコアを追加して、スコープの汚染を回避すべきです。最後に、InstanceMethodsモジュールの内部にはいかなる定数も追加しないでください。定数を追加するなら、プラグインモジュール自身に追加してください(上の例ではMarkdown
)。
プラグインを外部gemとして公開する予定がある場合は、外部公開向けの標準的なgem命名慣習に従うことをおすすめします。たとえばプラグインモジュール名がFooBar
であれば、gem名はroda-foo_bar
とすべきです。
Rodaではintrospectionをサポートしない
ルーティングツリーは、ルーティングをデータ構造には保存せず、ルーティングツリーのブロックを直接実行します。このため、ルーティングツリーを用いるときにルーティングに対してintrospection(訳注: 仏教などでは内観などと訳されることがあります)を行うことはできません。
Roda利用中にルーティングのintrospectionを行いたい場合は、roda-route_listという外部プラグインがあります。これを用いてルーティングファイルに適切なコメントを追加し、このプラグインに内蔵されているパーサーでこれらのコメントを解析してintrospection可能なルーティングメタデータに変換できます。
開発のヒントとなったもの
Rodaは、SinatraとCubaからインスピレーションを得ました。開発当初はCubaのforkという形で、ルーティングツリーを用いるアイデアはここから拝借しました(CubaはCubaで、Rumからアイデアをいただいています)。Sinatraからは、ルーティングブロックはリクエストのbodyを返すべきであり、そのルーティングはcanonicalであるべきというアイデアをいただきました。Rodaのプラグインシステムは、Sequelのそれをベースにしています。
Rubyバージョンのサポートポリシー
Rodaは、現在サポートされているRuby(MRI)およびJRubyを完全にサポートしています。
サポートされなくなったRubyやJRubyについてはサポートするかもしれませんが、サポートを継続すると問題になりうるマイナーバージョンでサポートをやめる可能性もあります。現時点のバージョンのRodaを実行するのに必要な最小限のRubyバージョンは、1.9.2です。
ライセンス
MIT
メンテナー
Jeremy Evans(code@jeremyevans.net)