Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: 高速/高性能ルーティングエンジンgem「Roda」README: 後編(翻訳)

概要

MITライセンスに基いて翻訳・公開いたします。


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はアプリのコードの変更が必須ではなく、requireconst_missingなど一部のコアメソッドを変更します。必要な設定は少なくて済む代わりに、Railsのファイルやクラスの命名規則に依存するので、これに従う必要があります。このライブラリは、存在しない定数にアクセスした場合にファイルを(オンザフライで)自動読み込みする機能も提供しています。アプリが自動読み込みに依存しないのであれば、依存関係のrequireにはrequire_dependencyを使わなければなりません。これ以外の方法では再読み込みされません。

AutoReloaderは、reloadable_pathsオプションのいずれかのエントリから到達するあらゆるファイルについて、透過的な再読み込み機能を提供します。この再読み込みは、トップレベルの定数を検出して、読み込み済みライブラリのうち再読み込み可能なライブラリに変更が生じた場合にこれらの定数を削除することによって行います。このライブラリが有効になると(通常はdevelopment環境で)、requirerequire_relativeがオーバーライドされます。必須の設定はreloadable_pathsだけです。

rerunshotgunは、いずれも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_dependenciesconfigureはどちらも、プラグイン呼び出しに渡される追加の引数やブロックを伴って呼び出されます。

つまり、インスタンスメソッドを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は、SinatraCubaからインスピレーションを得ました。開発当初は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


関連記事

RailsのCSRF保護を詳しく調べてみた(翻訳)

Ruby: 認証gem「Rodauth」README(更新翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。