学んだことをなぐり書き


コード文字列

# Rubyにとってコードは単なる文字列である
# Kernel#eval を使用すれば文字列をコードとして実行することが出来る
array = [10, 20]
element = 30
p eval("array << element")

# class_eval や instance_eval ではコード文字列とブロック両方使えるが
# 文字列だとコード文字列が評価されるまで構文エラーが出なかったり
# コードインジェクション(後述)のような脆弱性が潜む可能性があるので
# 特に理由がない限り出来るだけブロックの方がいい。

# 次のコードは if の end がないので syntax error が出る
l1 = lambda { 
#  if true
#    puts "true"
}

# eval だと文字列が実際に評価されないとエラーが出ない
l2 = lambda { 
  eval <<-EOS
    if true
      puts "true"
  EOS
}

# ここでエラー
#l2.call #=> SyntaxError
[10, 20, 30]
  • 参照
文字列で class(module)_eval を使った場合はクラス変数とクラス定数にアクセス出来る

Binding

# Kernel#binding を使用すれば現在のスコープを(Binding)オブジェクトの形で取得することが出来る
class C
  def m
    @x = 20
    y = 30
    binding
  end
end

b = C.new.m
p b.class
p eval "@x", b
p eval "y", b

# トップレベルのスコープが取得したい場合は Ruby の組込み定数 TOPLEVEL_BINDING を使用する。
class C2
  def get_self
    eval "self", TOPLEVEL_BINDING
  end

  def get_b
    eval "b", TOPLEVEL_BINDING
  end
end

obj = C2.new
p obj.get_self
p obj.get_b
Binding
20
30
main
#<Binding:0x1001695a8>

コードインジェクション

# eval は文字列をそのままコードとして実行することが出来る。
# 次のメソッドは Array のメソッドを使ってみるコードである。
# 入力された文字列を評価して出力している。
def explore_array(method)
  code = "['a', 'b', 'c'].#{method}"
  puts "Evaluating: #{code}"
  eval code
end

# 通常の Array のメソッドを呼ぶだけならば問題ないが、
p explore_array('find_index("b")')
p explore_array('map! { |e| e.next }')

# 次のような文字列を悪意のあるユーザーに入力されてしまったらどうなるだろう?
# プライベート情報がだだ漏れになってしまう。
# このような脆弱性を「コードインジェクション」という
p explore_array('object_id; Dir.glob("*")')

# コード文字列のパースによって、このような攻撃から守ることは可能だろうか?
# 基本的には「出来ない」と思っておいたほうが良い。
# 悪質なコードを書く方法は無数に存在する。

# 自分のコードにだけ eval を使えば問題はない。
# しかし、動いてるシステムで文字列が外部から来てないかを追跡するのは驚くほど難しい。
# 今回のような場合は eval を使わずに動的ディスパッチで書き直せる。
def explore_array2(method, *args)
  ['a', 'b', 'c'].send(method, *args)
end

p explore_array2('object_id')
#p explore_array2('object_id; Dir.glob("*")') #=> NoMethodError
Evaluating: ['a', 'b', 'c'].find_index("b")
1
Evaluating: ['a', 'b', 'c'].map! { |e| e.next }
["b", "c", "d"]
Evaluating: ['a', 'b', 'c'].object_id; Dir.glob("*")
["test.rb"]
2148222300
  • 参照
動的ディスパッチ

セーフレベル

# Rubyは潜在的に安全でないオブジェクト(特に外部から来たオブジェクト)に自動的に汚染の印をつける。
# 汚染オブジェクトには Webフォーム、ファイル、コマンドライン、システム変数などの文字列が含まれる。
# オブジェクトが汚染されているかどうかは Object#tainted?() メソッドで確認できる
p "hoge: #{"hoge".tainted?}"
print "User input: "
user_input = gets()
p "#{user_input}: #{user_input.tainted?}"

# eval でローカル変数の定義は出来ないっぽい。既にある変数の変更は可能
# キーボード入力で x の値を変更したとする
x = 0
eval user_input
p "x: #{x}"

# Rubyにはセーブレベルという機能がありグローバル変数 $SAFE に値を設定するとセーブレベルが変更できる。
# セーブレベルには0(デフォルト)から4まであり、高くなるほど制限が多くなる。

# デフォルトでは 0
puts "$SAFE = #{$SAFE}"
$SAFE = 1

# 0より大きいセーブレベルでは汚染された文字列をevalで評価できない。
#eval user_input #=>SecurityError

# 明示的に文字列の汚染を除去する場合は Object#untaint() を呼び出す。
user_input.untaint
eval user_input
"hoge: false"
User input: x = 5
"x = 5\n: true"
"x: 5"
$SAFE = 0

サンドボックス

# セーフレベルは一度上げてしまうと戻すことが出来ない。
# 試しに以下を実行すると SecurityError が出る。
#$SAFE = 2
#$SAFE = 0

$SAFE = 1

# これだとは使いづらいので、ある箇所だけセーフレベルを変更して評価したい場合は proc や lambda を使う。
sandbox = lambda {
  # lamdba の定義の前の設定されたセーフレベルを引き継ぐ
  # lamdba の定義の後に変更したセーフレベルは反映されない。
  p "sandbox before: #{$SAFE}"
  $SAFE = 2
  p "sandbox  after: #{$SAFE}"
}

# 既に lamdba を定義しているのでここでセーフレベルを上げても sandbox のデフォルトセーフレベルは 1 のままである。
# TODO: ここらへんを ruby のソースを引用して説明出来るようになりたい。
$SAFE = 3
p "main before: #{$SAFE}"
sandbox.call
p "main  after: #{$SAFE}"

# このようにセーフレベルが異なるeval用の特別な環境を「サンドボックス」と呼ぶ
"main before: 3"
"sandbox before: 1"
"sandbox  after: 2"
"main  after: 3"
  • 外部参照
http://d.hatena.ne.jp/keita_yamaguchi/20080622/121...

メンバーのみ編集できます