Status: Reproduced in a local test environment with a real generated RSA keypair/self-signed certificate and a real XML-DSig signature (via xml-crypto@2.1.6 ). Package: passport-saml-encrypted Affected Version: 0.1.13 Component: lib/saml.js ( SAML.prototype.validateSignature , SAML.prototype.validateResponse )
validateResponse() performs signature verification and assertion-content extraction as two independent XPath lookups over the same document, each selecting the first matching node in document order, with no cross-check that the Assertion node whose attributes are trusted is the same node (or is covered by) the Signature that was verified. This is the classic XML Signature Wrapping (XSW) vulnerability class. An attacker who possesses any validly IdP-signed SAML message can insert an additional, unsigned, forged Assertion earlier in document order; the library reports the signature as valid (because a real signed assertion still exists in the document) while extracting attacker-controlled identity/attributes from the unsigned forged assertion instead.
CWE-347: Improper Verification of Cryptographic Signature (XML Signature Wrapping subclass)
Prior art: this is the same root-cause family as the historical passport-saml advisory ( CVE-2017-11429 ).
Vector: AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N Base Score: 7.7 (High)
lib/saml.js:250-279 ( validateSignature ) picks the first anywhere in the document:
lib/saml.js:327-328 picks the first anywhere in the document, via a completely separate lookup:
Neither lookup is aware of the other. There is no code anywhere in lib/saml.js that resolves the signature's to a specific element and compares it (by object identity or ID) to the node used for profile extraction.
Root Cause Analysis (with real dependency verification)
The installed xml-crypto@2.1.6 ( node_modules/xml-crypto/lib/signed-xml.js , validateReferences() ) does defend against the textbook "duplicate ID" XSW variant: it throws if more than one element in the document shares the ID referenced by the signature's Reference URI . This defense is entirely internal to xml-crypto 's own reference-resolution and is never surfaced to lib/saml.js . Because saml.js:328 's assertion-extraction XPath is ID-blind and never consults xml-crypto 's resolved reference node, an attacker can insert a forged Assertion with no ID attribute at all (so xml-crypto 's duplicate-ID guard never fires — there is no duplicate to detect), positioned earlier in document order than the real signed assertion. xml-crypto validates the real assertion's signature successfully; saml.js independently and blindly picks the forged assertion for profile extraction.
Reproduction Environment
xml-crypto@2.1.6 (installed dependency, used for both signing the test fixture and library-side verification)
A locally generated, throwaway self-signed 2048-bit RSA keypair/certificate simulating the IdP's real signing key ( CVE_Reports/poc/certs/idp_key.pem / idp_cert.pem ), generated via: openssl req -x509 -newkey rsa:2048 -keyout idp_key.pem -out idp_cert.pem -days 3 -nodes -subj "/CN=test-idp"
No live IdP, network, or Docker deployment required — the SP-side vulnerable code is exercised directly, exactly as Strategy.prototype.authenticate() would invoke it on a real POST /login/callback per README.md .
Generate an IdP keypair (simulating the real IdP's private signing key) and configure the SP with the corresponding public certificate ( cert: idpCert ) — this time correctly set , unlike Finding How To Singout Idp #1 .
Build a genuine assertion for a normal user ( [email protected] , role: user ) and sign it with xml-crypto 's SignedXml using the IdP's private key (enveloped signature, exclusive C14N, SHA-1) — this is a real, valid XML-DSig signature that validateSignature() will successfully verify.
Build a second, entirely unsigned Assertion with a non-colliding ID and attacker-chosen content ( [email protected] , role: SUPERADMIN ).
Assemble a samlp:Response with the forged assertion placed first , followed by the real signed assertion .
Call saml.validateResponse() (with cert correctly configured) and observe that (a) the signature check succeeds (a real signature is found and validates), and (b) the returned profile contains the forged, unsigned identity — not the signed one.
Full runnable script: CVE_Reports/poc/poc2_xsw.js
Actual Captured Output
This is a full, real, end-to-end reproduction using an actual RSA keypair and an actual XML-DSig signature computed and verified by the real installed xml-crypto dependency — not a hypothetical description. cert was correctly configured, validateSignature() genuinely returned true , and the accepted profile is nonetheless the unsigned, attacker-forged one.
An attacker who can obtain any single validly-signed SAML message from the target's trusted IdP (e.g. by logging in normally as themselves) can impersonate an arbitrary other identity — including elevated-privilege roles — while the library's own signature check reports success.
Resolve the 's Reference URI to the exact element it covers (by ID ), and assert that this element is the same object as the Assertion / Response node subsequently used for profile extraction.
Reject documents containing duplicate ID attributes.
Prefer xml-crypto 's node-scoped verification API over raw-string checkSignature(xml) , and add positive test vectors from a known XSW payload corpus (e.g. SAML-Raider).
Somorovsky, J. et al., "On Breaking SAML: Be Whoever You Want to Be", USENIX Security 2012.
CVE-2017-11429 ( passport-saml , same root-cause family).
CWE-347:
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
