今年もRubyKaigiに協賛させていただきました!
Flatt Security 執行役員CCO / プロフェッショナルサービス事業部長の @toyojuni です。先日沖縄県那覇市で開催されたRubyKaigi 2024の振り返りと皆様への感謝の気持ちを込めて本記事を執筆します。
昨年に引き続いて、Flatt SecurityはRubyKaigiにPlatinum Sponsorとして協賛し、ブースを出展させていただきました。ありがたいことに、3日間でのブース訪問の延べ人数は500人を超え、様々なRubyistの方との接点を持てたと感じています。
そんな今回のブース出展の軸と言える企画が「YAMLパース占い」でした。
Flat Securityは明日から始まる #RubyKaigi 2024に協賛させていただきます!ブースでは新企画「YAMLパース占い」を実施します🔮
— 株式会社Flatt Security (@flatt_security) 2024年5月14日
与えられたYAMLをRubyでパースした結果を見事に見抜いた💎真のRubyist💎の方にノベルティを贈呈します。占いコンテンツは日替わりです♻️
3日間毎日お越しくださいませ! pic.twitter.com/p8sq3MsVD4
名前だけではあまりにも中身が想像できない本企画、当然ながら今回が初出しです笑
ふざけているようで実はしっかりとセキュリティ文脈のメッセージがあり、多くの来場者の方に「勉強になった!」と楽しんでいただけました。 本企画の意義に関して「確かブースで聞いたんだけど、泡盛飲みすぎて忘れちゃったな...」という方、ぜひ本記事で振り返っていただければ幸いです。
「YAMLパース占い」とは
パネル左側に示したYAMLを入力とした時に、Rubyでパースした結果はどれかを当てる、4択のクイズです。正解以外はRuby以外の言語やライブラリでパースされた結果です(後述の通り、ここで差分が存在するのが本企画のキモです!)。
ただ、現実的にこんなものは考えてもわかるわけがないので、実際には運試しだと思って当てずっぽうで回答していただきます。そのため企画の立て付けとしてもあくまで「占い」としています。見事正解された方にはオリジナルの大吉シールを名札に貼り、ノベルティをプレゼントさせていただきました。
また、RubyKaigiの3日間という会期をフルに活かすためにコンテンツ(与えられるYAMLと回答の選択肢)は日替わりとしました。おかげでたくさんの方にリピート訪問していただき、3日間毎日通い詰めてくださった方も何人もいて、運営としては嬉しい限りでした。
「YAMLパース占い」の意義
さて、当てずっぽうでしか回答できないものになんの意義があるんだという話ですが、意義の説明・企画趣旨は占い参加後にお渡しするチラシに記載していました。
スクリーンでは読みづらいと思うので引用します。
攻撃につながるような悪意のあるものでないか、ユーザーからの入力値を検証することはセキュアな実装のための基本中の基本と言えます。
ですが、複数の言語による処理が存在する環境で、言語Aで入力を検証していればその後は言語Bでは同じ入力値を検証せずとも安全だと考えてしまっていないでしょうか。しかし実際には、同じ入力値をパースした結果が言語ごとに異なる場合があります。それはすなわち「検証を行っている言語Aでは無害だが、言語Bの処理に対して攻撃可能」といった入力値の存在、それによる脆弱性につながることがあるのです。
GitLabのとある事例(CVE-2024-0402)ではRubyとGoでYAMLの解釈結果が異なることが原因となり、任意ファイル書き込みの脆弱性が発生していました。複数の言語による処理でデータを受け渡す場合は、各言語での検証を行いましょう。
ややニッチですが、今回はこのようなリスクをテーマとして取り上げさせていただいた次第でした。結論は最終文の通り「複数の言語による処理でデータを受け渡す場合は、各言語での検証を行いましょう」です。そうでなくとも、入力値を正しく検証することの重要性が少しでも伝わればと思います。
チラシと違ってリンクが貼れるので、参考リンクを以下にぶら下げます。特に、実践的な知識に関しては弊社の記事がオススメです!
▼ 該当CVEのNVDページ
▼ GitLab公式の解説ブログ
▼ 悪意のある入力値がトリガーとなるインジェクション系の脆弱性に関する弊社の解説記事
3日間の各コンテンツ解説
さて、拍子抜けな感じですが、本当にお伝えしたい内容はここまでで終わりです。これ以降は各日のコンテンツの解説、すなわち「なぜRubyはこのようにパースするのか、他の言語はなぜ異なるのか」の説明を行いますが、これはどちらかというと日常では役に立たない、オタク知識の部類に入るでしょう。
でもわかります、そっちが気になっちゃうのがエンジニアの性というものです。実際、これはこれで面白いんですよね。あくまで大事なのは「言語・ライブラリによる解釈差分が存在する」という事実であることを念頭に置きつつご覧ください。
1日目
パースされる YAML
price: 100 !!binary cHJpY2U=: 200 price: 300 !binary cHJpY2U=: 400
選択肢
No. | 言語・ライブラリ | パース結果 |
---|---|---|
1 | Ruby | {"price"=>400} |
2 | Perl(YAML) | {"price"=>100,"cHJpY2U="=>200} |
3 | Go(go-yaml/yaml) | {"price"=>300,"cHJpY2U="=>400} |
4 | その他多数の言語 | エラー |
解説
- YAMLにはtagという仕組みがあり、
!!binary
というtagはその後に続く文字列をBase64エンコードされた値として扱いますcHJpY2U=
はprice
をBase64エンコードしたものです
- さらにglobal tag, local tagという仕組みがあり、 local tag の処理は言語によって異なります。GitLabで発見された脆弱性はこれが問題でした
- デフォルトでは
!!
はglobal tagで、!
はlocal tagです
- デフォルトでは
- Rubyは後方優先の解釈を行う上に、local tagでのBase64デコードをサポートします。その影響でpriceが400まで上書きされる結果となります
2日目
パースされる YAML
isAdmin: yes <<: { isAdmin: no }
選択肢
No. | 言語・ライブラリ | パース結果 |
---|---|---|
1 | Python(PyYAML) | {"isAdmin"=>true} |
2 | Ruby | {"isAdmin"=>false} |
3 | Python(ruamel.yaml) | {"isAdmin"=>"yes"} |
4 | Go(goccy/go-yaml) | {"isAdmin"=>"no"} |
解説
以下の2観点で、言語やライブラリによって挙動が異なります1。
- YAML にマージという仕組みがあり
<<: {マージする値}
で表現されるが、元々存在した値とマージされた値でキーの衝突が起きた時どちらを優先するか - YAML に yes, no を true, false に変換する仕組みがあるが、これをサポートするか
各言語・ライブラリの挙動は以下の通りです。
- Python(PyYAML)
- 元々あった値(1行目)を優先します
- yes -> true の変換を行うため true となります
- Ruby
- マージで挿入された値(2行目)を優先します
- no -> false の変換を行うため、 false となります
- Python(ruamel.yaml)
- 元々あった値(1行目)を優先します
- yes -> true の変換を行わないため "yes" となります
- Go
- マージで挿入された値(2行目)を優先します
- no -> false の変換を行わないため、 "no" となります
3日目
パースされる YAML
a: +.inf b: -.inf c: ~ d: :symbol
選択肢
No. | 言語・ライブラリ | パース結果 |
---|---|---|
1 | Elixir(yaml_elixir) | {"a"=>:"+.inf", "b"=>:"-.inf", "c"=>nil, "d"=>:symbol} |
2 | PHP(symfony/yaml) | {"a"=>"+.inf", "b"=>-Infinity, "c"=>nil, "d"=>":symbol"} |
3 | Ruby | {"a"=>Infinity, "b"=>-Infinity, "c"=>nil, "d"=>:symbol} |
4 | Java(SnakeYAML) | {"a"=>Infinity, "b"=>-Infinity, "c"=>nil, "d"=>":symbol"} |
解説
YAML では +.inf は正の無限大を表す Float、-.inf は負の無限大を表す Float となるように定義されています。 しかし、これを文字列として扱ったり、Rubyにおけるシンボルのような識別子として扱ったりするような処理系も存在します。
また、コロンで始まる値を特別扱いするような仕様はありませんが、処理系によってはこれを特別扱いしてシンボルに変換するものもあります。
各言語の挙動は以下の通りです。
- Elixir(Elixir におけるアトムは Ruby におけるシンボルのように扱われる型です)
- +.inf, -.inf のどちらもアトムとして解釈します
- コロンで始まる文字列をアトムとして解釈します
- PHP
- +.inf は文字列として解釈しますが、 -.inf は数値として解釈します
- コロンで始まる文字列をそのまま文字列として解釈します
- Ruby
- +.inf, -.inf のどちらも数値として解釈します
- コロンで始まる文字列をシンボルとして解釈します
- Java
- +.inf, -.inf のどちらも数値として解釈します
- コロンで始まる文字列をそのまま文字列として解釈します
この3日目のコンテンツはマージやタグといった機能を使わずとも、リテラルの解釈ですら言語によって差分があるという示唆を与えてくれます。
おわりに
お楽しみいただけたでしょうか。本コンテンツは弊社で独自にこの分野の調査を行っている @lambdasawa が制作してくれました、ありがとうございました!
個人的な感想になってしまいますが、RubyKaigiは今年も非常に楽しかったです。本当に暖かさと活気にあふれたコミュニティだと思います。セキュリティという文脈で弊社は独自の面白さを提供できると思いますので、来年以降も何かしらの形で貢献できたら嬉しいなと思います。来年も松山でお会いしましょう!
お知らせ
Flatt Security ではWebアプリケーションをはじめとする、様々なプロダクトへのセキュリティ診断サービスを提供しています。仕様・実装に不安のある方はぜひお気軽にお問い合わせください。
上記のデータが示すように、診断は幅広いご予算帯に応じて実施が可能です。ご興味のある方向けに下記バナーより料金に関する資料もダウンロード可能です。
また、Flatt Security はセキュリティに関する様々な発信を行っています。 最新情報を見逃さないよう、公式X のフォローをぜひお願いします!
では、ここまでお読みいただきありがとうございました。
-
実は、yes/noおよびマージ(
<<
)に関する挙動はYAMLのバージョン1.1と1.2で異なります。1.2の仕様ではyes/noの変換がサポートされておらず、かつマージもサポートされていません。2日目の4つの解釈結果ではどれもキーが1つになっているので、この観点においてはどの言語・ライブラリも1.1の仕様に準拠してマージをサポートしていると言えます(サポートしていないなら"isAdmin"
と"<<"
の2つのキーが有るはずです)。しかし、解釈結果を見る限りGoとruamel.yamlの2つはyes/noの変換をサポートしておらず、この観点では1.2に準拠していると言えそうです。ここがチグハグなのでこの2つのパーサは1.1に準拠しているとも1.2に準拠しているとも言えず、言語・ライブラリによる解釈差分を生む原因になっていると言えそうです(そもそものYAMLのバージョンによる仕様差分が混乱の元なので、これでGoやruamel.yamlが悪いと責める気持ちにはなれませんが...)。↩