【実務・中級編】 SAMLアサーションの改ざん攻撃とXML署名検証の不備 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

脆弱な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データ抽出は、特定のパス以外を一切信用しない。

これらは決して「教科書的な教え」ではない。数多の脆弱性診断で「穴」を見つけてきた経験則に基づく、生存のためのルールだ。今日から君のコードを見直し、これらのチェックを実装に反映させてほしい。セキュリティとは、こうした細部の積み重ねの中にしか存在しないのだから。

コメント

タイトルとURLをコピーしました