脆弱なSAML実装を食い物にする:XML Signature Wrapping(XSW)攻撃の真実
「SAMLならセキュアだ」と信じているエンジニアほど、実は危うい足元で踊っている。
SAML認証は、IdP(Identity Provider)とSP(Service Provider)の間でXML形式のトークンをやり取りする仕組みだ。多くのライブラリが「署名があるから大丈夫」と謳うが、現実は甘くない。特にXML Signature Wrapping(XSW)は、署名検証のロジックと、その後のアプリケーションによるデータ抽出処理の「乖離」を突く、極めて狡猾な攻撃だ。
今日は、なぜあなたのSAML実装が突破されるのか、そしてどうやってそれを防ぐのか、現場の最前線から解説する。
—
1. XSW攻撃のメカニズム:なぜ「署名」がすり替えられるのか
XML Signature(XMLDSIG)は、ドキュメント全体ではなく、特定のノードのみを署名対象にできる。ここで攻撃者が狙うのは、「署名検証器が見ているノード」と「アプリケーションが実際に使っているノード」を別にするという手口だ。
攻撃のロジック
1. 正規のSAMLレスポンスを入手する: 攻撃者は自分の正当なアカウントでログインし、IdPから有効な署名付きSAMLレスポンスを取得する。
2. 構造を改ざんする: XMLツリー内に、正規の署名対象ノードを隠しつつ、攻撃者が用意した「悪意ある属性(例:管理者ID)」を持つノードを挿入する。
3. 検証の不備を突く:
- 検証ライブラリは、署名された正規のノードをチェックし「OK」を出す。
- しかし、後続のアプリケーション処理(XPath等)が、意図せず「攻撃者が混入させたノード」を参照してユーザー情報を取得してしまう。
結果、SPは「署名は正しい」と判断し、かつ「攻撃者が捏造したユーザー情報」でログインを許可してしまう。これがXSWの恐怖だ。
—
2. 実践的防御:Python (OneLogin SAML) での厳格化
多くの開発者がライブラリを使っているだろうが、ライブラリの設定こそが防壁の要だ。今回はPythonの python3-saml を例にする。重要なのは「署名検証の対象がドキュメント全体であること」を確認し、XPathの曖昧さを排除することだ。
# settings.json の設定例
# セキュリティの肝は、Strictモードと正しい署名検証の強制にある
saml_settings = {
"strict": True, # これをFalseにするのは自殺行為。必ずTrueにする。
"debug": False,
"sp": {
"entityId": "https://myapp.com/saml/metadata",
"assertionConsumerService": {
"url": "https://myapp.com/saml/acs",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
},
"idp": {
"entityId": "https://idp.example.com",
"singleSignOnService": {
"url": "https://idp.example.com/sso",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"x509cert": "..." # IdPの公開鍵をここに。絶対に直接ファイル読み込みで固定すること。
},
"security": {
"authnRequestsSigned": True,
"wantAssertionsSigned": True, # アサーションへの署名を必須にする
"wantMessagesSigned": True, # メッセージ全体への署名も必須に
"wantAssertionsEncrypted": True, # 暗号化を強制し、改ざん耐性を高める
"wantNameId": True,
}
}
—
3. なぜ「タイムスタンプチェック」を疎かにしてはいけないのか
署名検証が完璧でも、古いSAMLレスポンスを再送される「リプレイ攻撃」が残る。NotOnOrAfter や IssueInstant のチェックは単なる形式的な検証ではない。
堅牢なチェックのポイント(PHPの実装例)
以下のロジックを必ずAC(Assertion Consumer)サービスの実装に組み込んでほしい。
// SAMLレスポンス内の時刻チェック
$assertion = $samlResponse->getAssertion();
$now = time();
// NotOnOrAfterのチェック
$notOnOrAfter = strtotime($assertion->getAttribute('NotOnOrAfter'));
if ($now >= $notOnOrAfter) {
throw new Exception("SAMLレスポンスの有効期限が切れています。");
}
// IssueInstantのチェック(最大許容誤差は5分程度にする)
$issueInstant = strtotime($assertion->getAttribute('IssueInstant'));
$skew = 300; // 5分
if (abs($now - $issueInstant) > $skew) {
throw new Exception("時刻のズレが大きすぎます。不正なアクセスの可能性があります。");
}
—
4. 運用の現場で意識すべき「泥臭い」防衛術
コードだけでは防げないこともある。以下の項目は、インフラ・運用担当として必ず確認してほしい。
- 署名検証を迂回するXPathクエリを使わない:
自前でXMLをパースして //Assertion のようなクエリを書くと、XSWの標的になる。ライブラリが提供する、検証済みノードを特定するメソッドのみを使用すること。
- IdPのメタデータをキャッシュするな:
IdPの公開鍵(証明書)を動的に取得・キャッシュする実装は避ける。証明書がすり替えられたら終わりだ。設定ファイルに静的に書き込むのが最も安全。
- WAFでのブロック:
SAMLレスポンス(SAMLResponseパラメータ)に含まれるXML構造を監視する。不自然に二重の <Assertion> タグが含まれていたり、本来不要なノードが混入しているリクエストは即座に遮断せよ。
まとめ:防御の「聖域」を守る
SAMLの脆弱性は、複雑な仕様の「隙間」に潜んでいる。
1. ライブラリの Strict モードを盲信せず、署名対象を確認する。
2. 時刻検証は厳格に行い、リプレイ攻撃の芽を摘む。
3. 署名検証を終えた後のXMLデータ抽出は、特定のパス以外を一切信用しない。
これらは決して「教科書的な教え」ではない。数多の脆弱性診断で「穴」を見つけてきた経験則に基づく、生存のためのルールだ。今日から君のコードを見直し、これらのチェックを実装に反映させてほしい。セキュリティとは、こうした細部の積み重ねの中にしか存在しないのだから。
コメント