こんにちは、GMO Flatt Security の大崎です。
本記事では、私が作問し弊社の Kaigi on Rails 2025 出展ブースで出題したクイズを解説します。
このクイズは、実際に HackerOne に報告された脆弱性を元ネタとしています。
Ruby on Rails をバージョンアップした際に、Hash を JSON に変換する挙動が変わったことが原因でユーザー情報が漏洩してしまいました。さらに「なぜユニットテストで検知できなかったのか」についてもレポートで公開されており、とても学びの多い内容でした。そこで、このレポートを題材に今回のクイズを作りました。
問題

次のコードを、Rails 7で実行すると、/users/:id のレスポンスはどのようなJSONになるでしょうか? (Ruby 3.3.1)
class UsersController < ApplicationController def show user = { 'user' => { 'id' => 1, 'name' => 'Takumi', 'secret' => 'SECRET' } } filtered_user = { :user => { 'id' => 1, 'name' => 'Takumi' } } render json: user.merge(filtered_user) end end
選択肢
{"user":{"id":1,"name":"Takumi","secret":"SECRET"}}{"user":{"id":1,"name":"Takumi"}}{"user":{"id":1,"name":"Takumi","secret":"SECRET"}, "user":{"id":1,"name":"Takumi"}}- エラーになる
解答・解説を見るにはスクロールしてください!
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
解答
正解は…… 3. {"user":{"id":1,"name":"Takumi","secret":"SECRET"}, "user":{"id":1,"name":"Takumi"}} でした!
Ruby on Rails 6.1 までは Hash を JSON に変換する際、シンボルキーは文字列キーと同一視されており、同じ名前のキーが複数存在する場合は後から追加されたキーで上書きされていました。したがって、この場合の正解は 2. {"user":{"id":1,"name":"Takumi"}} でした。
しかし、 Ruby on Rails 7.1 では、シンボルキーと文字列キーを別のキーとして扱う仕様に変更され、両方のキーがそのまま JSON に出力されるようになりました。したがって、正解は 3. になります。
元ネタの脆弱性について
前述の通り、このクイズは実際に HackerOne に報告された脆弱性を元ネタとしています。
HackerOne は世界中で脆弱性報告に利用されている有名なバグバウンティプラットフォームですが、今回は Ruby on Rails で構築されている HackerOne 本体 に報告された脆弱性です。
HackerOne 自身もバグバウンティプログラムを実施しており、HackerOne 自体に報告された脆弱性を積極的に開示しています。
ちなみに、元ネタとなった脆弱性では、/reports/
※正式な報奨金額は公開されていませんが、Severity が 9.2 (Critical) と評価されています。
HackerOne における Critical 脆弱性の報奨金統計によると、以下の通りです。
HackerOne Security Stats
- Avg. bounty: $25,000.
- Rewarded submissions: 2.35%
- Typical range: $10,000 – $25,000
したがって、このケースも Critical 帯の相場通り $25,000 と推定できますが、あくまで参考値である点にご留意ください。
JSON における重複キーの扱い
クイズに挑戦してくださった方からは、「選択肢 3 のように “user” キーが重複している JSON は正しいのか?」という質問を多くいただきました。
実際のところ、JSON の仕様を定義している RFC 8259 では「オブジェクト内のキーは一意であるべき (SHOULD be unique)」と記載されています。
ただし MUST(必須)ではなく SHOULD(推奨) であるため、重複キーを含んでいても形式上は JSON として無効にはなりません。
また RFC 8259 では「キーが一意でない場合、そのオブジェクトを処理するソフトウェアの挙動は unpredictable(予測不可能)」と明記されています。
そのため、重複キーを含む JSON がどう解釈されるかは JSON パーサの実装次第 です。
会場では「undocumented」と説明してしまいましたが、正しくは RFC 8259 に“キーは一意であるべき”と明記されているが、重複キーが存在した場合に「最後の値を採用するのか」「エラーにするのか」といった挙動までは規定されておらず、ここはパーサーの実装依存 である、というのが正確な説明でした。🙇♂️
ユニットテストで検知できなかった理由
また、この挙動の差異は、ユニットテストでも検知されませんでした。理由としては、テストで利用していた approvals gem の検証方法にあります。approvals gem は 「スナップショットテスト」 を行うライブラリで、テスト対象の出力をファイルに保存しておき、次回以降のテストではそのスナップショットと比較して差分を検知します。このライブラリでは、JSON を検証する際に内部で一度 JSON.parse を行い、正規化した結果をスナップショットと比較します。
ところが、Ruby の JSON パーサは重複キーがあると 最後のキーだけを残す仕様 です。例えば次のように解釈されます:
require 'json' JSON.parse('{"my_key":"my_value","my_key":"my_other_value"}')
つまり、実際のレスポンスの JSON には重複キーと機密情報が含まれていたにもかかわらず、テストで比較されるのは「最後のキーだけを残したハッシュ」でした。
その結果、2 と 3 の両方の JSON をパースしても同一の結果となり、ユニットテストでは差異を検知できませんでした。
宿題クイズ
また、今回はKaigi on Rails会場で配布させていただいた解説チラシに、以下の「宿題クイズ」を掲載しました。
Q. Railsではなく、Rubyだけで以下を実行するとどうなるでしょうか?
require 'json' hash = { user: { id: 1, name: 'Takumi', secret: 'SECRET' }, 'user' => { id: 1, name: 'Takumi' } } puts JSON.generate(hash)
解答・解説を見るにはスクロールしてください!
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
↓
正解は…… {"user":{"id":1,"name":"Takumi","secret":"SECRET"}, "user":{"id":1,"name":"Takumi"}} でした!1問目と同じ結果です。
推測ではありますが、Ruby on Rails 6 から 7 にかけての挙動変更は、この Ruby 標準の動作に合わせたもの だった可能性があります。この点については、もう少し深掘りして調べてみたいと思います。
得られる学び
この脆弱性は、重複した JSON をどのようにパースするかというパーサーの挙動に依存するため、ユニットテストなどでは検知しにくいものでした。
一方で、ブラックボックス的に API のレスポンスを直接確認すると、機密情報が含まれていることがすぐに分かるため、発見は容易だったと考えられます。
脆弱性を見つける方法は、大きく分けて 静的検査(ホワイトボックス診断) と 動的検査(ブラックボックス診断) の 2 つがあります。静的検査はソースコードを解析して潜在的なリスクを検出する手法で、より深いリスクまで把握できる傾向があります。一方で動的検査は、実際の環境に対する入力と出力を観察して挙動を解析する手法であり、現実的な脆弱性を発見しやすい特徴があります。
静的検査が常に優れているわけではなく、それぞれに得意なパターンがあります。今回のケースでは、動的検査の方が圧倒的に発見しやすかったと思います。
脆弱性診断・ペネトレーションテストにおいては…?
なお、GMO Flatt Security では自分のようなセキュリティエンジニアによる脆弱性診断・ペネトレーションテストのサービスを提供していますが、このサービスの特徴もホワイトボックス診断とブラックボックス診断を組み合わせて実施することにあります。業界慣習としてはブラックボックスのみで行うところが多いところ、弊社の方法ではより深い検査が可能になりますしレポートにおけるわかりやすさも向上します。ぜひ、第三者機関による高度なリスクの洗い出しが必要な時にはお問い合わせください。
また、月額 7 万円で利用できるセキュリティ診断AIエージェント「Takumi byGMO」も提供しています。元々は GitHub リポジトリと連携させることで AI が高度に脆弱性を発見する静的検査(ホワイトボックス診断)の AI でしたが、近日中に動的検査の機能も搭載される予定です。静的検査だけでも Vim や Next.js といった著名 OSS のゼロデイ脆弱性を発見するほどに高性能ですが、動的検査の追加により更なる精度の向上が期待されます。
最新情報は公式 X アカウントで発信しています!ぜひ、フォローしてください。
それでは、ここまでお読みいただきありがとうございました。
コメント