How two XML parsers reading the same document from different angles broke SSO authentication for thousands of applications — without touching a single cryptographic primitive.
SAMLStorm is a pair of critical vulnerabilities in xml-crypto (≤ 6.0.0) and node-saml that allow a logged-in user to impersonate any other user — including administrators — without knowing their credentials or breaking any cryptography.
The attack works because the signature verification library (xml-crypto) and the assertion extraction library (node-saml) read different parts of the same XML document. The attacker exploits this split perspective to pass a valid signature check while the assertion content has already been swapped.
| Property | Detail |
|---|---|
| CVEs | CVE-2025-29775, CVE-2025-29774 |
| Affected packages | xml-crypto ≤ 6.0.0, node-saml (all versions using affected xml-crypto) |
| CVSS Score | 9.1 Critical |
| Attack requirement | Valid account on the IdP (any user) |
| Patch released | xml-crypto 6.0.1 — March 2025 |
| Known affected | WorkOS, multiple SaaS platforms using node-saml |
The SP only trusts an assertion because a cryptographic chain secures it. This is how it's supposed to work:
Each link secures the next. The DigestValue is the hash of the assertion. Change the assertion and the hash no longer matches — the signature check fails.
SAMLStorm breaks this chain — not through cryptography, but because xml-crypto and node-saml read different parts of the same document.
xml-crypto reads the DigestValue as a text node. When an XML comment is injected inside the element, it only reads the text after the comment — the real hash. But the assertion content has already been changed.
What xml-crypto sees: jMHaXYxasIl3oc5Q+VW0UYUCY6s= — the real hash. Check passes.
What node-saml extracts from the assertion: admin as the NameID.
serialize user {
nameID: 'admin', ← logged in as user1, arrived as admin
uid: 'admin',
email: 'user1@company.com', ← IdP attributes still from user1
eduPersonAffiliation: 'group1',
}xml-crypto and node-saml process the XML independently. When two SignedInfo nodes exist in the document, each library looks at a different one.
<ds:Signature> <FakeWrapper> <!-- injected by attacker --> <ds:SignedInfo> <!-- xml-crypto validates THIS one --> <ds:Reference URI="#fake"> <ds:DigestValue> 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= <!-- SHA256 of empty string — predictable, always the same --> </ds:DigestValue> </ds:Reference> </ds:SignedInfo> </FakeWrapper> <ds:SignedInfo> <!-- node-saml reads THIS one --> <ds:Reference URI="#realAssertion"> <ds:DigestValue>real hash...</ds:DigestValue> </ds:Reference> </ds:SignedInfo> </ds:Signature> <saml:Assertion> <saml:NameID>admin</saml:NameID> <!-- swapped by attacker --> </saml:Assertion>
SignedInfo (fake) and validates it — the DigestValue is SHA256(""), which always matches. node-saml reads the second SignedInfo (real) and extracts the assertion from it — which now has admin as the NameID. Two parsers, two perspectives on the same document.xml-crypto 6.0.1 was released the same day as the CVE disclosure — March 2025.
| Check | xml-crypto 6.0.0 — vulnerable | xml-crypto 6.0.1 — patched |
|---|---|---|
| Comment in DigestValue | ✗ Ignored, hash matches | ✓ Immediately rejected |
| Multiple SignedInfo nodes | ✗ Both accepted | ✓ More than one → error |
| Assertion source | ✗ Read from original XML | ✓ Read from verified content |
| HTTP status on attack | 302 → Login successful | 500 → Authentication failed |
npm audit would have been enough. The CVE and the patch shipped simultaneously. Not keeping your dependency stack current means losing control over your auth infrastructure — through a single stale npm version.PortSwigger Research extended the same attack class to Ruby and PHP stacks. The common root is identical.
| SAMLStorm (node stack) | The Fragile Lock | |
|---|---|---|
| Affected | xml-crypto / node-saml | ruby-saml / php-saml |
| Technique | Comment Injection, duplicate SignedInfo | Void Canonicalization, Attribute Pollution |
| Prerequisite | Valid login required | Only public IdP metadata needed |
| Patched | March 2025 | Dec. 2025 (CVE-2025-66568) |
| Real-world impact | WorkOS, various SaaS platforms | GitLab EE 17.8.4 (live demo) |
Want the full SAMLStorm writeup with lab setup and hands-on exploit walkthrough?
read the blog post →