Object#send 有害論

  • 45
    Like
  • 1
    Comment
More than 1 year has passed since last update.

ご存知の方には何を今更感があるかとは思いますが、パッとググった限り誰も書かれていなかったので、
Object#sendやそれとよく似たObject#public_sendの使い方は注意して使わなければ結構危ないセキュリティホールを作ってしまうよ、
というお話をしたいと思います。

TL;DR

Object#sendevalsystemの次ぐらいに危険です。ユーザーの入力など、外部から入力された値をObject#sendpublic_sendメソッドにそのまま渡すのはやめましょう。
これらのメソッドに渡す文字列は、(特殊なメタプログラミング用のライブラリを作る場合などを除いて)必ずどこかにハードコードした、信頼できるメソッドの名前のみにしてください。

危険なケース

例えばあからさまな例ですが、次のようなRailsのコントローラーのアクションがあったとしましょう。
みなさんはこれに近いようなコード、書いてませんよね?

def some_action
  FooModel.find_by_id(params[:id]).send(params[:method])
  ...
end

:warning: このような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のプログラムのほとんどあらゆる場所でputsexitが使えるのです。
結果、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からすべてのユーザーを削除できてしまいます :bomb:

public_sendじゃダメなの?

Rubyのメタプログラミングに詳しい方は、「Kernelにあるprivateなメソッドが危ないんだったら、privateが呼べない、public_sendにすればいいんじゃない?」と思われるかもしれません。
というか、私自身がこの記事を書く直前までそう思ってました。
ところがObjectクラスにも同じくらい危険なやつがいるんですよねぇ。
instance_evalです。
あまり使用されてない使い方だと思いますが、これにも第2引数として文字列を渡せば、Kernel#evalと同様任意のRubyコードが呼べてしまいます。
こいつはObjectクラスのpublicメソッドなので、public_sendからも普通に使えちゃうのです。

これ以外にも色々ありえます。

今回あげたものは一例にすぎず、sendするレシーバーのクラスの実装などによって、思わぬリスクを招くことがあるでしょう。
また、あなたが使っているライブラリや、同僚が調子にのるなどして加えた標準ライブラリへの修正によっても、いつの間にか落とし穴が掘られてしまうことがあります。
組み込みのものを含めあらゆるクラスを後から使う側で書き換えられてしまう、Rubyならではの問題です。

対策

前節で触れた通り、sendpublic_sendに任意の文字列を与えられるようにした場合のリスクは、どこからやってくるか分かりません。
ブラックリストによる回避では不十分な可能性が高いので、極力使用できる文字列のホワイトリストを用意して回避するよう努めましょう。
ハードコードしたSymbolしか渡さないようにするが一番簡単かつ確実でしょうね。
sendのユースケースは大体それでカバーできるでしょうし。


  1. 実際に終了した結果サーバーがどう振る舞うかは、Railsアプリを走らせるミドルウェア(Unicornとか、Passengerとか)の仕様に依存します。