CVE-2025-29775 CVE-2025-29774 xml-crypto ≤ 6.0.0 node-saml Authentication Bypass CVSS 9.1

SAMLStorm

How two XML parsers reading the same document from different angles broke SSO authentication for thousands of applications — without touching a single cryptographic primitive.

What happened?

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.

🔴
Impact: Any authenticated user can escalate to admin or any other account in the system. No password needed. No crypto broken. Just two parsers disagreeing about what they read.
PropertyDetail
CVEsCVE-2025-29775, CVE-2025-29774
Affected packagesxml-crypto ≤ 6.0.0, node-saml (all versions using affected xml-crypto)
CVSS Score9.1 Critical
Attack requirementValid account on the IdP (any user)
Patch releasedxml-crypto 6.0.1 — March 2025
Known affectedWorkOS, multiple SaaS platforms using node-saml

The Trust Chain

The SP only trusts an assertion because a cryptographic chain secures it. This is how it's supposed to work:

IdP Private Key
SignatureValue
SignedInfo
DigestValue
Assertion (user1)

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.

IdP Private Key
SignatureValue ✓
DigestValue ← read incorrectly
Assertion (admin) ← tampered
🔴
The core issue: The signature is mathematically valid — the IdP genuinely signed it. But xml-crypto verifies it against the wrong content. The SP thinks everything checks out and logs the attacker in as admin.

DigestValue Comment Injection

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.

Original — signed by the IdP
<ds:DigestValue> jMHaXYxasIl3oc5Q+VW0UYUCY6s= </ds:DigestValue> <saml:NameID> user1@company.com </saml:NameID>
Tampered — by the attacker
<ds:DigestValue> <!--forged-->jMHaXYxasIl3oc5Q+VW0UYUCY6s= </ds:DigestValue> <saml:NameID> admin </saml:NameID>

What xml-crypto sees: jMHaXYxasIl3oc5Q+VW0UYUCY6s= — the real hash. Check passes.

What node-saml extracts from the assertion: admin as the NameID.

⚠️
Why it works: XML comments are valid XML. The hash still matches — because xml-crypto 6.0.0 ignores the comment when reading and only takes the text node after it. The assertion itself was changed, but the hash check never fires.
What the server logs after a successful exploit Privilege Escalation
serialize user {
  nameID: 'admin',        ← logged in as user1, arrived as admin
  uid: 'admin',
  email: 'user1@company.com',  ← IdP attributes still from user1
  eduPersonAffiliation: 'group1',
}

Duplicate SignedInfo

xml-crypto and node-saml process the XML independently. When two SignedInfo nodes exist in the document, each library looks at a different one.

Tampered signature structure CVE-2025-29774
<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>
⚠️
The trick: xml-crypto finds the first 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.

One version number apart

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
Takeaway: 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.

The Fragile Lock — December 2025

PortSwigger Research extended the same attack class to Ruby and PHP stacks. The common root is identical.

SAMLStorm (node stack)The Fragile Lock
Affectedxml-crypto / node-samlruby-saml / php-saml
TechniqueComment Injection, duplicate SignedInfoVoid Canonicalization, Attribute Pollution
PrerequisiteValid login requiredOnly public IdP metadata needed
PatchedMarch 2025Dec. 2025 (CVE-2025-66568)
Real-world impactWorkOS, various SaaS platformsGitLab EE 17.8.4 (live demo)
🔗
Common root of both attacks: Signature verification and assertion processing use different parsers or different code paths on the same XML document. The attacker exploits the gap between what gets verified and what gets trusted. This is a structural weakness in how SAML libraries are built — not a one-off bug.
⚠️
Defence-in-depth: Even patched, SAML is complex. Complement it — enforce MFA at the IdP, monitor for impossible logins (same session, different NameID), and log all ACS endpoint hits with their raw SAMLResponse for forensic visibility.

Quiz

// test your understanding

1 / 8

score: 0

Want the full SAMLStorm writeup with lab setup and hands-on exploit walkthrough?

read the blog post →