Ruby の YAML.load が非互換になる(かもしれない)
タイトルは釣りっぽいんですが Psych v4.0.0 で『 Psych.load
が Psych.safe_load
を使用するようになった』事で普段利用している YAML.load
や YAML.load_file
が今後非互換になってしまう、という話です。
この変更により今まで読み込むことができていた YAML ファイルが今後読み込みエラーになる可能性があります。
先にまとめだけ書いておくと
- Psych v4.0.0 で
YAML.load
が非互換になる - なので既存の
YAML
データが読み込めずにエラーになる可能性がある - もし急に
YAML
データが読み込めなくなったらPsych
のバージョンを確認しよう - 回避する場合は
YAML.unsafe_load
などが利用できる
また、この記事の内容は記事を書いた当時の話なので今後変わっているかもしれないので注意してください。
NOTE: これは Ruby 本体の話というよりは『Psych
という gem が非互換になった』という話なので使用する Ruby のバージョン云々というよりかは『どの Psych
のバージョンを使用するのか』という話になってきます。
Psych って何?
Psych
は Ruby 本体の default gem で YAML ライブラリのバックエンド実装になります。
YAML
自体が Psych
に依存する形で実装されています。
require "yaml" # YAML.laod は実際には Psyche.load を呼び出している pp YAML.method(:load) # => #<Method: Psych.load(yaml, permitted_classes: ..., permitted_symbols: ..., aliases: ..., filename: ..., fallback: ..., symbolize_names: ..., freeze: ...) /home/worker/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/psych-4.0.0/lib/psych.rb:368> # そもそも YAML 自体が Psych になっている pp YAML # => Psych
なので本記事で書かれている Psych.load
などは実質 YAML.load
のことを指していることになります。
NOTE: default gem とは Ruby 本体に同梱されている Ruby のオフィシャルな gem です。
Ruby 本体とは別に管理されており、gem 単体でバージョンアップしたり古いバージョンを使用することができます。
Psych v4.0.0 で何が変わったのか
Psych.load
が内部で Psych.safe_load
を使用するように変更されました。
変更された背景としては Psych.load
は安全ではなく脆弱性につながるのでデフォルトで Psych.safe_load
を使用するようにしよう、みたいな話っぽいです。
また、既存の Psych.load
は Psych.unsafe_load
という名前のメソッドで使用することができます。
実際に変更が行われた PR で議論などがされていたので気になる方はそちらを参照ください。
NOTE: Psych.safe_load
は以前から存在している『安全に YAML データを読み込むため』のメソッドです。
Psych.load
と Psych.safe_load
の違いって?
Psych.safe_load
は Psych.load
よりもより安全に YAML データを読み込むためのメソッドです。
デフォルトでは以下のクラスのオブジェクトしか変換しません。
- TrueClass
- FalseClass
- NilClass
- Numeric
- String
- Array
- Hash
require "psych" require "date" pp Psych::VERSION # => "3.3.0" # String は問題ない pp Psych.safe_load("name: homu") # => {"name"=>"homu"} # Date は変換できずにエラーになる # error: `find': Tried to load unspecified class: Date (Psych::DisallowedClass) pp Psych.safe_load("date: 2020-01-01") # .load であれば問題ない pp Psych.load("date: 2020-01-01") # => {"date"=>#<Date: 2020-01-01 ((2458850j,0s,0n),+0s,2299161j)>}
これは明示的に『変換を許容するクラス』を第2引数や permitted_classes:
キーワード引数に渡すことで緩和することができます。
require "psych" require "date" pp Psych::VERSION # => "3.3.0" # 明示的に Date を指定することで変換できる pp Psych.safe_load("date: 2020-01-01", [Date]) # => {"date"=>#<Date: 2020-01-01 ((2458850j,0s,0n),+0s,2299161j)>} # Psych v3.1.0 (Ruby 2.6)以降であればキーワード引数で渡せる pp Psych.safe_load("date: 2020-01-01", permitted_classes: [Date])
また、 &default
のようなエイリアスもデフォルトでは許可されていません。
require "psych" require "date" pp Psych::VERSION # => "3.3.0" data = <<~EOS default: &default aaa: aaa bbb: bbb development: <<: *default test: <<: *default production: <<: *default EOS # OK: 読み込める pp Psych.load(data) # => {"default"=>{"aaa"=>"aaa", "bbb"=>"bbb"}, # "development"=>{"aaa"=>"aaa", "bbb"=>"bbb"}, # "test"=>{"aaa"=>"aaa", "bbb"=>"bbb"}, # "production"=>{"aaa"=>"aaa", "bbb"=>"bbb"}} # NG: 読み込めない # error: Unknown alias: default (Psych::BadAlias) pp Psych.safe_load(data)
これは Psych.safe_load(data)
の第4引数や aliases: true
を渡すことで許容する事ができます。
require "psych" require "date" pp Psych::VERSION # => "3.3.0" data = <<~EOS default: &default aaa: aaa bbb: bbb development: <<: *default test: <<: *default production: <<: *default EOS # OK: 読み込める pp Psych.safe_load(data, [], [], true) # Psych v3.1.0 (Ruby 2.6)以降であればキーワード引数で渡せる pp Psych.safe_load(data, aliases: true)
このような違いが Psych.load
と Psych.safe_load
にはあるため Psych.load
が Psych.safe_load
を使用するようになると『今まで読み込めていた YAML データが読み込めなくなってしまう』可能性があります。
実際に先程のコードを Psych v4.0.0 で使用するとエラーになります。
require "psych" require "date" pp Psych::VERSION # => "4.0.0" # Date は変換できずにエラーになる # error: `find': Tried to load unspecified class: Date (Psych::DisallowedClass) pp Psych.load("date: 2020-01-01")
require "psych" require "date" pp Psych::VERSION # => "4.0.0" data = <<~EOS default: &default aaa: aaa bbb: bbb development: <<: *default test: <<: *default production: <<: *default EOS # 読み込めない # error: Unknown alias: default (Psych::BadAlias) pp Psych.load(data)
NOTE: Psych v3.1.0 (Ruby 2.6)以降であれば Psych.safe_load
に permitted_classes
や aliases
などキーワード引数で渡せるようになりました。
また、以前のオプション引数は v4.0.0 から『削除された』ので注意する必要があります。
基本的にはキーワード引数を使っておけば大丈夫そう。
どういう影響があるのか
これに関しては bugs.ruby に以下のチケットができていました。
このチケットでは先程書いたエイリアスに関する非互換な変更が問題視されているみたいですね。
実際に Rails や Rubocop でもこの変更に対する対応がすでに行われているようです。
- Fix ruby-master test suite (Psych 4.0.0) by casperisfine · Pull Request #42257 · rails/rails · GitHub
- Fix a build error for Ruby 3.1.0-dev by koic · Pull Request #9806 · rubocop/rubocop · GitHub
余談:エイリアスはなぜ安全ではない?
件の bugs.ruby に載っていたんですが例えばエイリアスを使うと次のように再帰的なデータ構造を定義することができます。
require "psych" require "date" pp Psych::VERSION # => "3.3.0" input = <<~EOS default: &default - *default EOS # 再帰的なデータ構造になる result = Psych.load(input) pp result # => {"default"=>[[...]]} # データが永続的に続く pp result["default"][0][0][0][0]
このようなデータを次のように再帰的に読み込もうとするとクラッシュしてしまします。
require "psych" require "date" pp Psych::VERSION # => "3.3.0" def process thing thing.each do |item| case item when Array process item when Hash # ... when String # ... end end end input = <<~EOS default: &default - *default EOS # error: stack level too deep (SystemStackError) process Psych.load(input)
このような問題があるため Psych.safe_load
ではエイリアスが許可されていません。
この変更に対する対処方法
以前と同じように YAML データを読み込む対象方法として以下の2つがあります。
1. バージョン指定して古いバージョンを使用する
Psych 自体は gem なのでそれ単体でバージョンを指定して使用する事ができます。
意図的に v4.0.0 未満を使用したい場合は以下のように Gemfile
に書くなどすれば回避できます。
gem "psych", "< 4.0.0" # 逆に 4.0.0 以上を使用したい場合はこんな感じ # gem "psych", ">= 4.0.0"
これは一時的な対処なのでできれば次に紹介する対処方法を行ったほうがよいでしょう。
2. Psych.unsafe_load
を使用する
Psych.load
ではなくて Psych.unsafe_load
を使用することで Psych v4.0.0 以降でも以前の Psych.load
を模倣する事ができます。
require "psych" require "date" pp Psych::VERSION # => "4.0.0" data = <<~EOS default: &default aaa: aaa bbb: bbb development: <<: *default test: <<: *default production: <<: *default EOS # OK: unsafe_load を使うと以前と同じように読み込める pp Psych.unsafe_load(data)
また、ファイルから YAML データを読み込む場合は Psych.load_file
の代わりに Psych.unsafe_load_file
も利用できるので必要に応じて使い分けるとよいと思います。
Rails や Rubocop はこの Psych.unsafe_load
を使用して対応を行っています。
基本的にはこの Psych.unsafe_load
と Psych.unsafe_load_file
を使って対処することになると思います。
また Psych.unsafe_load
と Psych.unsafe_load_file
なのですがこのメソッドは Psych v3.3.2 で追加されています。
なので移行パスとしては
1. Psych v3.3.2 に上げる
2. 必要に応じて Psych.unsafe_load
や Psych.unsafe_load_file
を使うように変更する
3. Psych v4.0.0 に上げる
という風に更新するといいのではないでしょうか。
結局今後どうなるの?
件の bugs.ruby のチケットでは『Ruby 3.0 と 3.1 で互換性を維持したいので Ruby 3.1 では Psych v4.0.0 を使用しない』という旨のコメントがされています。
なので意図的に Psych のバージョンを上げない限りは特に問題はでないかも…?
あくまでも現時点(2021/05/23)での方向性なので今後もしかしたら方針が変わるかもしれませんので注意してください。
まとめ
- Psych v4.0.0 で
YAML.load
が非互換になる - なので既存の
YAML
データが読み込めずにエラーになる可能性がある - もし急に
YAML
データが読み込めなくなったらPsych
のバージョンを確認しよう - 回避する場合は
YAML.unsafe_load
などが利用できる