ご存知の方には何を今更感があるかとは思いますが、パッとググった限り誰も書かれていなかったので、
Object#send
やそれとよく似たObject#public_send
の使い方は注意して使わなければ結構危ないセキュリティホールを作ってしまうよ、
というお話をしたいと思います。
TL;DR
Object#send
はeval
やsystem
の次ぐらいに危険です。ユーザーの入力など、外部から入力された値をObject#send
やpublic_send
メソッドにそのまま渡すのはやめましょう。
これらのメソッドに渡す文字列は、(特殊なメタプログラミング用のライブラリを作る場合などを除いて)必ずどこかにハードコードした、信頼できるメソッドの名前のみにしてください。
危険なケース
例えばあからさまな例ですが、次のようなRailsのコントローラーのアクションがあったとしましょう。
みなさんはこれに近いようなコード、書いてませんよね?
def some_action
FooModel.find_by_id(params[:id]).send(params[:method])
...
end
このようなactionのparams[:method]
に例えばexit
という文字列を送った場合、なんとRailsアプリを終了させることができてしまいます 1。
これは、Kernel
モジュールのexit
メソッドを、意図せず呼び出してしまったことによります。
上記のようなModelに限らず、ほぼ任意のRubyのオブジェクトに対してこのメソッドは使用できるので、下記のようなコードをREPLに書いて試してみるとよいでしょう。
1.send 'exit'
REPLが終了しちゃいましたよね?
どうしてこんなことができてしまうのでしょう?
その原因は、RubyのほとんどすべてのオブジェクトはKernel
モジュールをinclude
しているところにあります。
RubyのKernel
というモジュールには、問題のexit
を始め、puts
など、プログラムのどこからでも呼べるメソッド(いわゆるグローバルな関数)が定義されています。
Object
クラスはこのKernel
モジュールをinclude
しているため、Rubyのプログラムのほとんどあらゆる場所でputs
やexit
が使えるのです。
結果、send
に任意の文字列を渡せるようなコードを書いてしまうと、Object
が使える任意のメソッド、すなわちKernel
モジュールのメソッドが呼べてしまい、思いの外大きな脆弱性を作ることになってしまうのです。
もっと危険なケース
Kernel
にはいろいろな機能があるため、やり方が悪いともっと危険なメソッドを攻撃者に呼ばせることができてしまいます。
先程のsend
メソッドを呼ぶRailsのアクションに、パラメーターを追加したとしましょう。
def some_action
FooModel.find_by_id(params[:id]).send(params[:method], params[:arg])
...
end
Kernel
モジュールではsystem
メソッドもeval
メソッドも使えるため、
今度はOSコマンドインジェクションや、Rubyコードインジェクション(っていう呼び方でいいのでしょうか...)ができてしまいます。
早い話が任意コード実行です。
攻撃者がparams[:method]
にeval
を、params[:arg]
にUser.delete_all
とでも渡せば、あなたのDBからすべてのユーザーを削除できてしまいます
public_send
じゃダメなの?
Rubyのメタプログラミングに詳しい方は、「Kernel
にあるprivate
なメソッドが危ないんだったら、private
が呼べない、public_send
にすればいいんじゃない?」と思われるかもしれません。
というか、私自身がこの記事を書く直前までそう思ってました。
ところがObject
クラスにも同じくらい危険なやつがいるんですよねぇ。
instance_eval
です。
あまり使用されてない使い方だと思いますが、これにも第2引数として文字列を渡せば、Kernel#eval
と同様任意のRubyコードが呼べてしまいます。
こいつはObject
クラスのpublic
メソッドなので、public_send
からも普通に使えちゃうのです。
これ以外にも色々ありえます。
今回あげたものは一例にすぎず、send
するレシーバーのクラスの実装などによって、思わぬリスクを招くことがあるでしょう。
また、あなたが使っているライブラリや、同僚が調子にのるなどして加えた標準ライブラリへの修正によっても、いつの間にか落とし穴が掘られてしまうことがあります。
組み込みのものを含めあらゆるクラスを後から使う側で書き換えられてしまう、Rubyならではの問題です。
対策
前節で触れた通り、send
やpublic_send
に任意の文字列を与えられるようにした場合のリスクは、どこからやってくるか分かりません。
ブラックリストによる回避では不十分な可能性が高いので、極力使用できる文字列のホワイトリストを用意して回避するよう努めましょう。
ハードコードしたSymbol
しか渡さないようにするが一番簡単かつ確実でしょうね。
send
のユースケースは大体それでカバーできるでしょうし。
-
実際に終了した結果サーバーがどう振る舞うかは、Railsアプリを走らせるミドルウェア(Unicornとか、Passengerとか)の仕様に依存します。 ↩