OpenStructに信頼できない値を渡してはいけない
新しいOpenStruct
に信頼できない値を渡すと、GCされないシンボルが作成されメモリが使いつくされる可能性があります。
対象のバージョン
対象となるostruct
gemのバージョンは、0.3.0かそれ以上です。
Ruby 3.0にはostruct
のバージョン0.3.1が添付されているため、この対象となります。
Ruby 2.7とそれ以前のRubyのバージョンでは、これよりも古いバージョンのostruct
が添付されているためデフォルトでは対象になりません。
しかし、Ruby 2.7でもgem install ostruct
してバージョン0.3.1をインストールでき、その場合は対象となります。
Problem
ostruct
gem v0.3.0以上では、OpenStruct.new
に渡したHashのキーに対応するメソッドを、OpenStruct#initialize
が呼ばれたタイミングで定義するようになりました。
これはこのHashのキーのシンボルがGCされなくなることを意味します。 なぜならば、Rubyではメソッド名として使われたシンボルはGCされないためです。 この問題については https://fiveteesixone.lackland.io/2015/01/21/symbol-gc-ruby-2-2/ などが詳しいでしょう。
これは次のようなプログラムで確かめることができます。
# test.rb require 'ostruct' require 'objspace' puts RUBY_DESCRIPTION puts OpenStruct::VERSION 10000.times do |i| OpenStruct.new(:"#{'x' * 10000}_#{i}" => i) end GC.start puts "#{ObjectSpace.memsize_of_all * 0.001 * 0.001} MB"
このプログラムではそれぞれ異なる10001文字のキーを持つハッシュを10000回作成し、それをOpenStruct.new
に渡しています。
その後GC.start
でGCを実行した後に、Rubyプロセス全体のメモリ使用量を計測しています。1
まずはこれをRuby 2.7.2と、それに標準で添付されているostruct
gemで検証します。この場合のメモリ使用量は7MBほどでした。
$ ruby test.rb ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux] 0.2.0 7.028303 MB
次に、このコードをRuby 3.0.0と、それに標準で添付されているostruct
gemで検証します。するとメモリ使用量は204MBまで増加しました。2
$ ruby test.rb ruby 3.0.0dev (2020-12-22T00:22:38Z master 843fd1e8cf) [x86_64-linux] 0.3.1 204.02889000000002 MB
Ruby 2.7.2でgem install ostruct
した場合にも、同様の結果が得られます。
$ gem install ostruct Fetching ostruct-0.3.1.gem Successfully installed ostruct-0.3.1 $ ruby test.rb ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux] 0.3.1 203.90093900000002 MB
これは先にも説明したとおり、大量に作成されたシンボルがGCの対象ならず、GC.start
の後も残り続けているのが原因です。
この問題はostruct
のソースコード中のコメントにも明記されています。
This is a potential security issue; building OpenStruct from untrusted user data (e.g. JSON web request) may be susceptible to a "symbol denial of service" attack since the keys create methods and names of methods are never garbage collected.
https://github.com/ruby/ostruct/blob/v0.3.1/lib/ostruct.rb#L79-L81
なぜメソッドが定義されるようになったのか
これはOpenStruct
の挙動の変更が原因です。
Ruby 2.7.2までのOpenStruct
の実装では、OpenStruct#initialize
時にメソッドを定義するのではなく、method_missing
を通して実際にそのメソッドが呼ばれたときに初めてメソッドを定義していました。
メソッドの定義が呼び出されるまで遅延されていれば、OpenStruct.new
がどんな入力を受け取っていてもそれを呼び出さない限りGCできないシンボルは生まれません。
ところが、この実装では問題がありました。 https://bugs.ruby-lang.org/issues/15409
詳しくチケットを見ていないのですが、Kernel
に生えているのと同名のキーを持つHashをOpenStruct
に渡した場合、Kernel
に生えているメソッドが優先されてしまっていました。
そのため、OpenStruct#initialize
時にKernel
で定義されたメソッドを上書きして定義する必要がありました。
この変更は https://github.com/ruby/ostruct/pull/15 で行われています。
回避策
まず、Ruby 3未満を使っていて、ostruct
gemをインストールしていない場合、影響はありません。
ただしRuby 3にアップグレードすることを考えると、対応を考えたほうが良いでしょう。
また、OpenStruct
が固定のキーしか受け付けない場合にも影響はありません。
該当のバージョンのostruct
gemを使っていて、かつOpenStruct
がユーザー入力を直接受け付けるような場合には対応が必要です。
まず、ostruct
gemのバージョンを古いものに固定するとこの問題は解決します。
Gemfile
にgem 'ostruct', '< 0.3'
のように書くと良いでしょう。
ostruct
gemはバージョン0.1.0がリリースされているため、この場合にはバージョン0.1.0がインストールされます。
また、OpenStruct
をそもそも使わないことを検討しても良いでしょう。移行先にはただのHashやStructなどが考えられます。
OpenStruct
には今回紹介した以外にもパフォーマンスの問題などがあり、ドキュメントでも別の手段を使うことを検討するよう書かれています。
(略) For all these reasons, consider not using OpenStruct at all.
https://github.com/ruby/ostruct/blob/v0.3.1/lib/ostruct.rb#L107
まとめ
Ruby 3.0.0のOpenStructの変更にセキュリティ上気になる点があったので記事にしました。
今まで信頼できない値を渡しても安全だったOpenStruct
がそうでなくなったのは気をつける必要があると思います。
その反面この情報があまり知られていない(私は今日まで知りませんでした)と感じたため、今回記事にしました。
この問題に気がついたのは、Junichi Itoさんが書かれたRuby 3の変更点を紹介する記事を読んでいる途中でした。 https://zenn.dev/jnchito/articles/24e0bd7fd1045d#%E6%A8%99%E6%BA%96%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E4%B8%BB%E3%81%AA%E5%A4%89%E6%9B%B4%E7%82%B9 分かりやすい記事をありがとうございます。
また、当初この問題がostruct
gemのドキュメントに明記されている(問題として認識され広く公開されている)ことに気が付かず、セキュリティ上の問題としてHackerOneから報告をしてしまいました。
Ruby 3のリリース直前という忙しい時期にお手数をおかけしました…。
この記事によって問題を未然に防ぐ手助けができましたら望外の喜びです。
-
メモリ使用量の計測は https://blog.freedom-man.com/measure-ruby-memory-usage のコードを参考にしました。↩
-
Ruby 3.0.0.rc1でテストするのが筋ですが、手元にインストールされていなかったのでmasterブランチをビルドしたものを使いました。↩