設計上、安全ではないSAML

2021/08/16 08:47

Joonas Loppi
フィンランドのソフトウェアエンジニアであり起業家
この記事は、著者の許可を得て配信しています。
 SAML is insecure by design

SAMLとは何なのか?

SAML(Security Assertion Markup Language)は、異なるインターネット ドメイン間でユーザー認証情報を交換するための標準規格です。

上の説明はウィキペディアからの抜粋です。

SAMLは、シングルサインオン(SSO)(「Googleでサインインする」、「Twitterでサインインする」などの場合)によく使われます。つまり、例えば「〇〇.com」にログインしたい場合、「〇〇.com」は外部の認証プロバイダーを信頼して使用し、ユーザーのIDをアサ―トすることができるということです。SAMLは、これらの認証およびIDの詳細な情報を組織の境界(ウェブ・ドメイン)を越えて通信するためのものなのです。

更新:GoogleはSAMLに対応していますが、GoogleとFacebookは、一般公開されている「Googleでログイン」の認証フローに主にOAuth2を使用しています。シングルサインオンの意味を説明するための例としてGoogleの例を使用したことを明確にしなかったのは私の責任です。

なぜ注意する必要があるのか?

SAML は非常に多くの場所で使用されており、おそらくお客様のセキュリティにも影響を与えているからです。

SAMLには、非常に大きな影響を与える壊滅的な脆弱性があります。例えば、私の理解が正しければ(セキュリティ研究者が私のツイートをリツイートしていたので、私の意見はおそらく正しいでしょう)、フィンランドの税務当局、ほぼ全ての政府サービス、健康記録システムに脆弱性があり、攻撃者は人々の納税申告書、健康記録など、基本的にオンラインで利用可能な政府関連のあらゆるデータを盗み見ることができました。

脆弱性が利用されなかった(あるいはそのような事例が発見されなかった)ためか、このことはメディアではほぼ無視されています。

なぜSAMLは安全ではないのか?

SAML は、計算値に基づく署名を使用しているからです。その署名は本質的に安全ではないため、SAMLの設計は安全ではないということです。

なぜ計算値の署名は危険なのか?

要約:ある計算された特性に基づいたセキュリティにすると、その計算の欠陥や違い、曖昧さが悪い影響を及ぼします。計算が複雑になればなるほど、その危険性は増すのです。SAMLの署名の計算は非常に複雑です。

ではコンセプトの説明に移ります。擬似的なIDを取り上げてみよう(実際のSAML はXMⅬなんですが…)。

$ cat assertion.json
{
  "signed_in_user": "Joonas"
}

単なるバイト群として上記のファイルに署名することができます。

$ cat assertion.json | sha1sum
e58dc03a7491f9e5fb2ed664b23d826489c42cc5

ここで、ファイルを少しだけ変更してみましょう({の前にスペースを追加しました)。署名が変わったことが分かります。

$ cat assertion.json
 {
  "signed_in_user": "Joonas"
}
$ cat assertion.json | sha1sum
0bc80a9ee02f611b70319c9fe12b7e504107354a

これは非常に優れた特性である。なぜなら、セキュリティ上重要な文書(SAMLがそうである)に対するいかなる変更であっても(JSONレベルでは意味がないと考えられる変更であっても)、理想的には異なる署名を作成するために必要だからです。

この性質は、ノン・マリーアビリティと呼ばれています。マリーアビリティの一般的な定義は、次のようになっています。

粘土の持つ柔軟性のように、壊れずに他のものに形を変えられる性質のこと

私たちがドキュメントに生のバイトblobとして署名することで、これをノン・マリーアビリティなものにします。これは、情報セキュリティにおいては望ましいビヘイビアです。

SAMLは、その署名が計算値に基づいているため、可鍛性があります。

例の 署名 生のコンテンツはマリーアビリティである セキュリティ
生のバイト ファイルやメッセージ、生のコンテンツ いいえ 👍
計算された値 解析されたXMLツリーのコンテンツ はい 👎

例を挙げて説明するために、まずJSONの例に戻って考えてみましょう。jq(JSON変換ユーティリティー)を使って、ドキュメントの中から持ってきた何かを計算してみましょう。

$ cat assertion.json
 {
  "signed_in_user": "Joonas"
}

$ cat assertion.json | jq .
{
  "signed_in_user": "Joonas"
}

(jq. は、ドキュメント全体を再印刷することを意味します。)

ファイルを jq に通すと、スペースが削除されることに気付きましたか?これは、JSONレベルではスペースが重要ではないからです。一見、面白くなさそうですが、私たちは危険な領域に向かっています。

計算された値に署名してみましょう。

$ cat assertion.json | jq . | sha1sum
e58dc03a7491f9e5fb2ed664b23d826489c42cc5

ファイルにスペースの修正が残っていても、署名は元の署名(スペースが追加されていないファイルのもの)と一致しています。

なぜこれが危険なのでしょうか?もう一度、ファイルを変更してみましょう。

$ cat assertion.json
{
  "signed_in_user": "EvilAttacker",
  "signed_in_user": "Joonas"
}

$ cat assertion.json | jq . | sha1sum
e58dc03a7491f9e5fb2ed664b23d826489c42cc5

# the above is because:

$ cat assertion.json | jq .
{
  "signed_in_user": "Joonas"
}

署名は元のファイルと一致したままです。これは、重複したキーが有効なJSONであり、処理時に削除され、ほとんどのJSON実装では最後のキーが勝つことになります。

では、SAMLドキュメントを処理する2つの異なるコードがあり、JSONの重複キー(=メッセージのセマンティックコンテンツ)に関する解釈やパーサーのビヘイビアが異なる場合はどうなるでしょうか?

攻撃者は ID プロバイダーに自分のためにアサーションへの署名を依頼しましたが、SAML の可鍛性により、パーサーの違いを攻撃して文書を改ざんしました。署名のバリデーションには有効であるけれども、別のユーザーのデータにアクセスすることができました。

これで、可鍛性および計算/解釈されたコンテンツに基づく署名がいかに危険であるかを説明できたと思います。

実際のSAMLの脆弱性は?

これらのSAMLの脆弱性で何が起こったのか、というJSONの例ほど分かりやすくはありませんが、それは、これらの脆弱性の原理とその根本原因である「計算値への署名と可鍛性」について説明しています。

最新の脆弱性は、XMLラウンドトリップの不安定性によるものでした(「XML ラウンドトリップの脆弱性とは」を参照ください)。

要約すると、この脆弱性は、XMLを解析した後にXMLを書き込むと、意味的に異なるドキュメントが生成されることに起因します(すなわち、「 encode(decode(xmlDocument)) != xmlDocument)」ということです。)

100%確実ではありませんが、SAML署名のバリエーションにはXMLの書き込みのステップが必要なので、次のようになったのだと思います。

上記は、署名されるべき SAML コンテンツが非可鍛性を持つのであれば攻撃ベクトルにはなりません。つまり、ID プロバイダーがその文書に署名した後の変更は、署名違反として検出されることになります。

なぜSAMLはこのようになったのか?

SAMLの設計者は非可鍛性であることが良い特性であることを知っていたと善意で仮定し、それでも可鍛性を持つ設計になってしまった理由を推測してみましょう。

では、何かに署名してみましょう。何かに署名すると、出力として署名が得られます:sign(contentToSign, signingKey) -> signature.

署名を有効にするためには、  contentToSign と一緒に signature を転送する必要があります。そうするとコンシューマ―がcontentToSignを読んだときにsignatureでバリデーションできるようになります。

署名だけで送信すると、簡単に可鍛性ではない状態を維持することができます。

contentToSign

しかし、 signature がどこかに消えてしまいました。SAML の設計者は、SAML ドキュメントとその署名を別々に転送したくなかったのでしょう (署名は HTTP ヘッダーや URL パラメーターに含まれている可能性があります)。そのため、利便性を考慮して、同じ XML ドキュメントに署名を埋め込んだのです。

samlDocument
├── contentToSign
└── signature

より技術的な観点で正確に言うと、それよりもさらにYOLO(人生は一度キリ、だから最大限に楽しもうじゃないか)的になります。署名は contentToSign,の下に保存されているので、バリデーション処理の際に署名を無視して(これもまた危険な複雑さ)、実際には contentToSign に含めないようにする必要があます。 contentToSign は解決不可能な再帰的な問題になります。7

samlDocument
└── contentToSign
    └── signature

しかし、 signature が contentToSign 内に保存されていないという、前にあったより単純なケースを想像してください。署名のバリデーションをバイトベースにすることができたかどうかという疑問に立ち返ってみましょう。

問題は、XMLメッセージの中からcontentToSign に属するバイトを抽出するのが非常に難しいことです。私の知る限り、XML パーサー API はこの使用例に対応していません。たとえサポートしていたとしても、SAML を有用なものにするためには、ほとんどの XML パーサーの実装に対応するものに対応していなければいけません。

=>  samlDocument があって、そのサブツリーである contentToSignにアクセスしようとすると、そこには XML レベルのアクセスしかできません。そのため、SAML の設計者はそのことをあまり気にせず、 🤷♂️(お手上げ状態)になり、「じゃあ XML レベルのデータに署名しよう」と考えたのでしょう。

XMLパーサーの出力に署名することは非常に難しいのです。なぜならば、XMLライブラリや言語ごとにパーサーが異なるXMLパーサーの出力から、署名の入力を安定させようとするからです。そのため、SAML実装が署名をバリデーションするバイトシーケンスに対して安定したコンセンサスを得られるように、XML属性をめちゃくちゃな順序でソートするなどのルールを持つXML dsigが存在するのです。結局のところ、常にバイト単位での照合が必要だということです。このおかしなことは正規化されており、次のように変換されます。

<Example   foo="hello"        bar="hehehe">
	<Item>    mooo</Item  >
	</Example>

このようにバイトに変換します(署名入力が安定します)。

<Example bar="hehehe" foo="hello"><Item>mooo</Item></Example>

(これは私が考案した例で、実際にどのようなルールが存在するかはわかりませんが、ここではいくつかの例を紹介します)。

まとめ:XMLサブツリーは署名やバリデーションが難しく、それを可能にするための恐ろしいものがあり、経験的な証拠が示すように、それはセキュリティ上の悪夢としか言えません。

私は、このようなアプローチを使用しているものはすべて壊れており、安全ではないと考えるべきだと思っていますし、公言しても問題ありません。

脆弱性の緩和

Goの脆弱性に関して言うと、GoのXMLスタックにおけるラウンドトリップの不安定性を修正しなければなりませんでした。また、安全対策として、XMLを実際に処理する前にラウンドトリップの安定性をバリデーションする必要がありました。

要約すると、バイト群からの署名をバリデーションするのではなく、SAMLの署名バリデーションには以下のものが必要だということです:

  • ラウンドトリップの安定性バリデーション(=XML構文解析+エンコード)
  • XML パーシング(もう一度)
  • XML の正規化(エンコーディングですが、特定の複雑なルールと変換を伴うXML dsig)

複雑だと思う人がいても仕方ありません。みなさんが思った通り、とても複雑だからです。複雑であればあるほど、バグやセキュリティ問題が発生する可能性が高くなります。

SAMLはどのように設計すべきだったのか?

では、どう設計すべきだったのか解説します。私は素人なので、私の考えが絶対正しいというわけではないので、大目に見てくださいね。

(注:この投稿はすべて疑似コードであり、本物のSAMLではありません。興味のある方は実際の例をご覧ください。)

次のようにする代わりに、

<SAMLSignedDocument>
	<SAMLSignature>e58dc03a7491f9e5fb2ed664b23d826489c42cc5</SAMLSignature>
	<SAMLContentToSign>
		<Assertion>
			<UserId>Joonas</UserId>
		</Assertion>
	</SAMLContentToSign>
</SAMLSignedDocument>

(正しく安全に署名・バリデーションすることが難しいことが分かりました)

 <Assertion> を受け取り、そのサブツリーをバイトにシリアライズし、base64などで保存することで、バイトとして転送し、署名がバリデーションされた後にXMLパースすることができます。

<SAMLSignedDocument>
	<SAMLSignature>e58dc03a7491f9e5fb2ed664b23d826489c42cc5</SAMLSignature>
	<SAMLContentToSign>PEFzc2VydGlvbj48VXNlcklkPkpvb25hczwvVXNlcklkPjwvQXNzZXJ0aW9uPgo=</SAMLContentToSign>
</SAMLSignedDocument>

私はXMLについての理解が乏しいので、文字列やバイトデータを転送するためのもっと素晴らしい方法を知りませんが、これで十分だと思います。

この方法では、すべてが1つのXML文書の中にあるという特性を維持することができましたが、XMLパーサーを2回行う必要があります。

  1. まず外側の文書、それから次にバイトブロブに対して署名をバリデーションします。
  2. 署名が一致した場合は、バリデーション済みの内側の文書をパーサーします。

確かに、XMLの中にXMLを文字列やバイトとして保存すると醜いと主張する人もいるかもしれませんが(もちろん私もそういう人のうちの一人です)、とにかく私たちが成し遂げたことを見てください。 SAMLContentToSign 内のすべてのデータは可鍛性がなくなり、信頼できるソースからのデータであることをバリデーションする前に、セキュリティクリティカルなデータをパースする必要はありません。また、「XML dsig」のようなものも必要ありません。

SAMLの奇妙なところ

SAMLでは、XMLドキュメントのルートが署名されていない、つまりアサーション要素にのみ署名するというユースケースに対応することが求められます。攻撃者管理のデータを許可する目的は何でしょうか? このようなケースでは、安全ではないデータを破棄するための追加コードが必要になります。なぜなら、そんなデータを使ってしまうと大惨事になってしまうからです。

SAMLは最悪なのに使われている理由

私には分かりません。私はこの分野にそこまで詳しくはありませんが、他にもっといいものがあるとも思いません。

OAuth2は存在しますが、リソースへの承認を得ることを目的としており、それ自体は認証/IDプロトコルではありません。その違いについてはこちらをご覧ください。

また、OpenID Connectというものもあります。

私が思うに、ある基準が普及すると、より良い選択肢があったとしても、以前の選択肢がすでにクリティカルマス(商品やサービスが広く普及するために、最低限必要とされる供給量)に達しているため、移行するのは難しいのではないかと思います(WhatsappとSignalの関係も同じことが言えます)。

行動する

SAMLを廃止しましょう。🗑️ OAuth2やOpenID Connectを勧める専門家もいるようです。

ベンダーがSAMLを提供してくる場合は、代替案を求めましょう。

知らぬが仏

どんなことでも、学べば学ぶほど、バブルガムとガムテープで補強されているだけだということが分かりました。正直なところ、とても不安になります。

このテーマについて調べていたとき、フィンランド政府のウェブサイトのセキュリティは、JavaScript(TypeScriptでもない)で実装されたシングルサインオンコンポーネントに依存していることにも気づきました。それは次のようなものでした。

appstore
googleplay
会員登録

会員登録して、もっと便利に利用しよう

  • 1.

    記事をストックできる
    気になる記事をピックして、いつでも読み返すことができます。
  • 2.

    新着ニュースをカスタマイズできます
    好きなニュースフィードをフォローすると、新着ニュースが受け取れます。