パーサ差異を利用したSAML SSO認証回避技術
(github.blog)- ruby-saml 1.17.0以下で見つかったCVE-2025-25291・CVE-2025-25292は、有効な署名1つだけで任意ユーザーとしてのログインを可能にし、SAML SSO環境におけるアカウント乗っ取りリスクを高める
- 脆弱性は、署名検証経路でREXMLとNokogiriが同じXML文書内の異なる
Signature要素を解釈できてしまう、パーサ差異に起因する SignedInfo、SignatureValue、assertionのハッシュ、DigestValueが異なるパーサの結果から組み合わされることで、各検査は通過してもハッシュと署名の結び付きが断たれる可能性がある- GitHubはruby-samlの再導入を検討する過程でバグバウンティとSecurity Labレビューを実施し、GitLabで悪用可能なインスタンスを発見してセキュリティチームへ通知した。現在GitHubの認証ではruby-samlを使用していない
- ユーザーはruby-saml 1.18.0へ更新する必要があり、
omniauth-samlのようにruby-samlを参照するライブラリも、修正版を参照するリリースへあわせて更新する必要がある
ruby-saml認証回避の影響
- ruby-saml 1.17.0以下で重大度の高い認証回避脆弱性2件が確認された
- CVE-2025-25291
- CVE-2025-25292
- 攻撃者は、対象組織のSAML responseまたはassertionの検証に使われる鍵で生成された有効な署名1つさえあれば、自前でSAML assertionを作成し、任意のユーザーとしてログインできる
- 想定される署名の入手元は次のとおり
- 権限の低い別ユーザーの署名済みassertionまたはresponse
- 場合によっては、公開アクセス可能なSAML IdPの署名済みmetadata
- この脆弱性は最終的にアカウント乗っ取り攻撃に悪用され得る
- GitHubは現在認証にruby-samlを使っていないが、SAML認証にオープンソースライブラリを再利用する案を検討する中でruby-samlを評価していた
- ruby-samlは他の人気プロジェクトや製品でも使われており、GitHubはGitLabで悪用可能なインスタンスを発見してGitLabのセキュリティチームへ通知した
GitHubが再びruby-samlを検討した理由
- GitHubは2014年までruby-samlを使用していたが、当時必要な機能が不足していたため、自前のSAML実装へ移行した
- その後、自前実装でも暗号化assertion関連の脆弱性であるCVE-2024-9487のようなバグバウンティ報告があり、GitHubはruby-saml再導入の検討を始めた
- 2024年10月には、ahacker1が発見したruby-saml認証回避脆弱性CVE-2024-45409が公開された
- GitHubはruby-samlへの移行可能性をより綿密に評価するため、非公開バグバウンティを開始した
- 選定された研究者に対し、ruby-samlでSAML認証を行うGitHubのテスト環境へのアクセスを提供した
- GitHub Security Labもruby-samlの攻撃対象領域をあわせてレビューした
2つのXMLパーサが生んだ検証不一致
- コードレビューの過程で、ahacker1とGitHub Security Labは、ruby-samlの署名検証経路で2つのXMLパーサが併用されていることを確認した
- REXML: 純粋なRubyで実装されたXMLパーサ
- Nokogiri: libxml2、libgumbo、JRuby向けXercesなどを包むAPIを提供し、XMLとHTMLのパースをサポートする
- 問題の経路は
xml_security.rbのvalidate_signatureメソッドである - このメソッドは、最初の
Signature要素と実際のSignatureValueをREXMLで読み取るREXML::XPath.first(@working_copy, "//ds:Signature", {"ds"=>DSIG})
- 一方で同じ検証フローの中で、
SignatureとSignedInfoはNokogiriで再取得され、正規化されるdocument.at_xpath('//ds:Signature', 'ds' => DSIG)noko_signed_info_element.canonicalize(canon_algorithm)
- assertionはNokogiriで抽出・正規化・ハッシュ化される一方、比較対象となる
DigestValueはREXMLから取得される - 最終的な検証材料は次のように分断される
- assertionはNokogiriで抽出・正規化されたうえでハッシュ化される
- 比較対象のハッシュはREXMLが読んだ
DigestValueに由来する SignedInfoはNokogiriで抽出・正規化されるSignatureValueはREXMLで抽出される
SAML署名検証で断たれたセキュリティ上の結び付き
- SAML responseは、IdPからSPへログインユーザー情報をXML形式で渡す
- HTTP POST bindingを使う場合、SAML responseはユーザーのブラウザを経由してSPへ届くため、ユーザーがメッセージを改ざんできないよう署名検証が必要になる
- 単純化したSAML responseでは、重要な情報は通常
Assertion内のSubjectとNameIDに含まれる - 一般に、assertionまたはSAML response全体に署名できる
- assertionに署名される場合、検証は2段階で行われる
Signatureを取り除いたassertionを正規化してハッシュ化し、DigestValueと比較するSignedInfoを正規化し、SignatureValueで署名を検証する
- 今回の脆弱性では、この2段階がそれぞれ通過しても、同じデータを保証できない
- ハッシュは実際のassertionのハッシュかもしれない
- 署名は別の
SignedInfo要素に対する署名かもしれない
- 必要なセキュリティ特性は、ハッシュ化された内容・ハッシュ・署名が直接結び付いていることであり、検証後は実際に検証された部分からのみ情報を読み取るべきである
実際のエクスプロイト構成方法
- 核心となる条件は、REXMLとNokogiriが同じXML文書内で**異なる
Signature**を見てしまうようにすることで、実際にそれが可能だった - ahacker1はバグバウンティ参加中、パーサ差異を利用した動作するエクスプロイトを先に作成した
- MattermostのJuho Forsénが2021年に公開したXML roundtrips vulnerabilitiesから着想を得た
- GitHub Security Labは、Trail of BitsのRubyファザーruzzyを使い、別のパーサ差異ベースのエクスプロイトを作成した
- 例示されたエクスプロイトでは、
StatusDetail要素の中にNokogiriにしか見えない追加のSignatureを入れる - 検証フローは次のように分離される
- Nokogiriが見るsignatureの
SignedInfoが正規化される - REXMLが見るsignatureから抽出した
SignatureValueで検証される - NokogiriがIDで見つけたassertionが正規化・ハッシュ化される
- REXMLが読んだ
DigestValueとハッシュが比較される
- Nokogiriが見るsignatureの
- 結果として、有効な
SignedInfoと有効な署名が互いに整合し、改ざんされたassertionとその計算済みdigestも互いに整合するため、ruby-samlはそのassertionを受け入れてしまう
緩和策と検知の限界
- NokogiriでSAML responseをパースする際にパースエラーを確認すれば、現在知られている一部の非公開エクスプロイトを防げる
- Nokogiriのパースエラーは例外として発生しないため、パース済み文書の
errorsメンバーを直接確認する必要がある - 例示コードでは
Nokogiri::XML::ParseOptions::STRICT | Nokogiri::XML::ParseOptions::NONETオプションを使用し、doc.errors.any?のときにエラーを発生させる - この方法は完全な修正ではないが、少なくとも1つのエクスプロイトを実行不能にする
- 信頼できる侵害指標は現時点で知られていない
- 潜在的な指標の1つはdebug類似環境でしか動作しない
- これを公開すると、動作するエクスプロイト実装の詳細を過度に明かすことになるため公開していない
- 推奨される確認方法は、SP側でユーザーの想定位置と一致しないIPアドレスからのSAMLログインなど、不審なログインを探すことである
修正方針と更新対象
- 初期修正では、API互換性の問題からXMLパーサの片方を削除しなかった
- より根本的な問題は、ハッシュ検証と署名検証の分離にあり、この分離がパーサ差異を通じて悪用可能になっていたことだ
- XMLパーサの片方を削除する作業は別の理由ですでに計画されており、追加改善とあわせてメジャーリリースで行われる可能性がある
- ruby-saml利用者は、修正を含む1.18.0へ更新する必要がある
- ruby-samlを利用するライブラリもあわせて確認すべきである
- 例: omniauth-saml
- 当該ライブラリも、修正済みruby-samlバージョンを参照する版へ更新する必要がある
- GitHub Security Labは今後、GitHub Security Lab repositoryで概念実証エクスプロイトを公開する予定である
公開と対応のタイムライン
- 2024-11-04: ruby-samlでSAML認証を評価していたGitHubのテスト環境に対し、認証回避を実証したバグバウンティ報告が受理された
- 2024-11-04: 潜在的な緩和策の特定とテスト作業が開始された
- 2024-11-12: 最初の緩和計画を無力化する2つ目の認証回避が発見された
- 2024-11-13: ruby-saml maintainerのSixto Martínへ初回連絡を行った
- 2024-11-14: 2つのパーサ差異がruby-samlに報告され、maintainerが即座に応答した
- 2024-11-14: maintainerとahacker1が潜在的なパッチ作業を開始した
- 初期案の1つはXMLパーサの片方を削除することだったが、後方互換性を壊さずには実現できなかった
- 2025-02-04: ahacker1が後方互換性のない修正案を提案した
- 2025-02-06: ahacker1が後方互換性のある修正案も提案した
- 2025-02-12: GitHub Security Lab advisoryの90日期限が終了した
- 2025-02-16: maintainerが後方互換性を維持しつつ理解しやすい修正方針での作業を開始した
- 2025-02-17: ruby-samlのリリースとGitLabオンプレミス製品のリリースを調整するため、GitLabへ初回連絡を行った
- 2025-03-12: 修正版ruby-samlがリリースされた
1件のコメント
Hacker Newsのコメント
GitHubのSAML実装は役に立たないと思う
本来の意図は個人アカウントをエンタープライズに持ち込んで使うことだが、GitHubサイト内ではある程度動作するものの、GitHubでログインしたアプリが組織レベルで承認された後は、組織メンバーシップを読み取るのを防げない
SAMLセッションが必要なのは、そのアプリがそのユーザーから受け取ったトークンでデータを取得するときだけで、実際にはSASTツールはほぼ常にアプリインスタンストークンを使い、組織内にGitHubアカウントさえあればコードを見せていた
Tailscaleは知らせると修正し、Sonarcloudは誰にも言わないでほしいと言い、GitHubは数週間後に「完全に想定された挙動」だと返答したが、知らせたどのベンダーもそうは理解しておらず、ドキュメントもGitHubの回答と矛盾していた
セキュリティバグの報告は偶然見つけても報われない仕事で、これを職業にするのは想像しにくい
GitHubがごくまれに業務用PRのマージ時に個人メールアドレスをデフォルトで使ってしまったことがあり、誰かに聞かれたら、GitHubであれどこであれ、個人用と業務用を同じアカウントに混ぜるなと助言している
リポジトリ/コミット/PR分析ツールであるhttps://dev.log.xyzを作る中で、「GitHubで見られるものだけをDevlogでも見られる」状態に合わせるのに多大な労力がかかり、全体の体験は非常に苛立たしいものだった
GitHubのOAuth権限セレクターも過度に混乱を招くもので、「GitHubでログイン」するときに、どの組織のどの情報を共有するのか確信しにくい
JavaScriptに慣れた若い開発者に、クライアント側検証だけでは十分でないと説得するのも難しく、機能要件と予算を決める事業者に対してはなおさら難しい
どんな形のパスワード保管庫であれ、物理的な保管庫やパスワードの再利用でさえ、結果的により安全になり得る
結局、ミニマリズムがまた勝ったということだ
“GitHub doesn’t currently use ruby-saml for authentication, but began evaluating the use of the library with the intention of using an open source library for SAML authentication once more”とある
最近SAML実装をしなければならなかったが、このタイトルにはまったく驚かない
SAML仕様自体はかなり合理的だが、その基盤であるXML署名、さらにはXML正規化は、標準と呼べるなら本当に狂気の沙汰だ
こういうねじれた堕落した仕様は委員会にしか作れないし、一個人の頭ではここまで矛盾した考えを抱え込んで結合することはできなかったと思う
署名をアウトオブバンドで送信するだけでも、SAMLは実装して楽しいものになっていただろう
XMLは文字通りeXtensible Markup Languageなのに、SAML標準化委員会はその上に自分たちだけの拡張メカニズム言語をさらに発明した
認証Cookieに入る情報と大差ないごく小さなデータのために、プロトコルの上にさらにプロトコルを載せるのは、最大級に官僚的な委員会だけが生み出せる特殊な種類の愚かさだ
それぞれ別のアカウントにログインするという発想自体は問題なさそうだ
アカウント同士を連結して、一度に大規模に侵害され得る構造にするのは、根本的により危険だ
SAML、より広くはXML-DSIGは、広く使われているセキュリティプロトコルの中で文字通り最悪だと思う
一般にはどんなコストを払ってでもOAuthに移行すべきで、少なくとも新製品を市場に出す際にこれに依存することは拒むだろう
非常に危険で、実用的な形式検証で突破口が出ない限り、今回が最後のDSIG脆弱性、あるいは最悪の脆弱性だとは想像しにくい
暗号には触れてすらいないのに、すでにエンタープライズソフトウェア的思考の頂点だ
誰かがWS-*やOASIS/XACMLのようなもののメーリングリストと標準化団体の惨状を深く掘るべきだ
それで合っている?
うわ、ほかに選択肢がない場合を除き、誰もREXMLを使うべきではない
不正なXMLでも喜んでパースしてしまい、その後の段階で無限に多くの問題を引き起こす
文字通り正規表現でXMLをパースしており、なぜそれをしてはいけないのかを示す優れた事例だ
プロジェクトがNokogiriを使い始めたのは性能のためではなく、正確性のためだった
正規言語ではないものを正規表現でパースするな、ということだ
最近o3をテストしたが、特定のコードブロックでライブラリと関係ない問題を直そうとするたびに、そのブロックが使うライブラリを何度も変えていた
Sonnetではそうした現象は見なかった
こうして修正の過程で、コードベースや標準ライブラリにすでに存在するためテストは通り、Gemfileの変更も不要な、より劣ったライブラリ/gemに置き換わり、問題が入り込みやすそうに見える
SAMLは設計上安全ではない
以前からもっとよく説明した記事があり、たとえば https://joonas.fi/2021/08/saml-is-insecure-by-design/ がある
この古いスレッドで印象に残っている文句は「意味ではなくバイトに署名せよ」だった
パーサー間の差異は予測可能で、時には不可避でもある
署名済みレスポンスから何を得ようとしているのかが非常に重要だ
現代のTLSにおけるジレンマの一つは、時には社内CAを1つ信頼したい場合があり、それは簡単な道だが、パートナーのCA証明書を受け入れ、パートナーが複数になると、もはやエンドエンティティ証明書だけを見るわけにはいかず、チェーンのルートも意思決定において同じくらい重要になる、という点だ
なので可能なら AWS署名アルゴリズム も避けることを勧める
V4は理論上は安全だが、AWSは2度失敗しており、SigV1とSigV3は設計上安全ではなかったにもかかわらず、どういうわけか設計レビューを通過して公開されてしまった
素晴らしい記事だ
記事でも触れられているが、ahacker1 には大きな拍手を送るべきだ
SAML実装を安全にするため、非常に精緻で価値のある仕事をしており、SSOReadyも彼の作業に本当に感謝している
今週初めにはWorkOSも、ahacker1との協業について良い記事を公開していた: https://workos.com/blog/samlstorm
「GitLabでこの脆弱性が悪用可能な事例として見つかり、セキュリティチームに知らせた」という部分があるが、気になる人向けに言うと、GitLabは修正版 をすでに出している
https://about.gitlab.com/releases/2025/03/12/patch-release-g...
関連記事として、Latacoraの2019年の記事 How (not) to sign a JSON object[1] がある
要するに、ツリー構造をネストして署名 するのは難しく、落とし穴が多い
エンベロープがメッセージを生の文字列として保持し、その生の文字列に署名するほうが簡単だ
[1]: https://www.latacora.com/blog/2019/07/24/how-not-to/
ここでのもっと単純な結論は、署名があるべき場所で署名を探せ、ということではないのか?
「//ds:Signature」のように過度に一般的な XPath を使って、想定外の場所にあるどんな署名でも見つけてしまうようにするのではなく
危険な構成要素だけを外科的に取り除くのではなく、風呂の水と一緒に赤ん坊も少し捨てる必要があり、大きく切除して抗がん治療をするように対処すべきだ
誇りあるIT管理者なら、今後の計画から SAML は外すべきだ
SSOという発想全体も疑わしいし、XMLパースは週に2回殴られたのだから今後は避けるべきだ
XMLをJSONに置き換える方針の何が問題なのか?
設計、プロトコル、データ形式の段階からそうすべきだ
一般的なWeb開発の習慣や設計上の考慮事項は、セキュリティコードに必要なものとは合わず、正常なコードを書くために必要なものともしばしば正反対だ
ブログ記事で脆弱性を説明しているのに、肝心の問題になった パーサー差異 を省いているのは少し苛立たしい
話の導入部を書いておきながらクライマックスを省略したようなものだ
(...//ds:DigestValue).firstChild.nodeValueをしていながら、.firstChildがNodeかどうかを確認しておらず、問題になったケースではCommentだったそのため正規化されていない側は「隠れた」署名を見て、コメントを捨てた修正済みの側はNodeを見た。2つの実装が署名済み文書について異なる判断をすると、笑えない事態が起きる
具体的な内容は近いうちに出てくるだろう