SAML XML Signature Wrapping: Why Signature Validation Is Still Hard in 2025
SAML remains the backbone of enterprise single sign-on. A service provider trusts an identity provider to assert “this user is [email protected] and belongs to the finance group.” That trust is expressed through an XML digital signature. When the signature validation logic and the assertion processing logic disagree about which element was signed, the result is an authentication bypass. This class of flaw is called XML Signature Wrapping (XSW), and 2025 produced a remarkable cluster of CVEs showing that mature libraries still get it wrong.
1. The XSW Primitive
A SAML response contains one or more <saml:Assertion> elements. A signed assertion includes a <ds:Signature> with a <ds:Reference URI="#assertion-id"> pointing at the assertion's ID. A correct verifier must:
- Locate the signature.
- Resolve the reference URI to exactly one element.
- Canonicalize that element.
- Verify the digest and the signature over the canonical form.
- Process the same element that was verified.
XSW breaks step 5. The attacker keeps the original signed assertion intact — so the cryptographic check passes — but inserts a second, attacker-controlled assertion that the application later reads. Variants move the signature, wrap it around a different element, duplicate IDs, or exploit namespace and canonicalization differences. The signature is valid; the identity is not.
A simplified wrapped response
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="legit">
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:Reference URI="#legit">...</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>...</ds:SignatureValue>
</ds:Signature>
<saml:Subject><saml:NameID>[email protected]</saml:NameID></saml:Subject>
</saml:Assertion>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="evil">
<saml:Subject><saml:NameID>[email protected]</saml:NameID></saml:Subject>
<saml:AttributeStatement>
<saml:Attribute Name="role"><saml:AttributeValue>admin</saml:AttributeValue></saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
A verifier that finds the first signature, validates it against #legit, and then extracts the first assertion it encounters may be safe. A verifier that validates the signature but extracts the last assertion, or that uses a different XPath for verification and processing, is vulnerable. The gap between those two code paths is the entire attack surface.
2. The 2025 CVE Cluster
CVE-2025-25292 — ruby-saml parser differential
The ruby-saml library was affected by an authentication bypass caused by a parser differential between REXML and Nokogiri. Different XML parsers can resolve namespaces, entities, and duplicate IDs differently. An attacker crafts a document that one parser interprets as containing a valid signature over the original assertion while the other parser selects a different assertion for processing. The issue was fixed in ruby-saml 1.12.4 and 1.18.0 and is rated 9.3 in public advisories. A closely related issue, CVE-2025-25291, covered the REXML path.
CVE-2025-47949 — samlify signature bypass
The Node.js samlify library before version 2.10.0 allowed a signature-wrapping style bypass that could authenticate an attacker as an arbitrary user. Public scoring places it at 9.9. The fix enforces that the signed assertion is the assertion that is consumed, rather than trusting a separately selected node.
CVE-2025-54419 — node-saml
node-saml through 5.0.1 contained an authentication bypass fixed in 5.1.0, with a CVSS score of 10.0 in the advisory. The severity reflects the typical deployment: a service provider that accepts a forged assertion may grant administrative access without any credential.
CVE-2025-23369 — GitHub Enterprise Server
GitHub Enterprise Server had an improper cryptographic signature verification issue in SAML authentication, fixed in 3.12.14, 3.13.10, 3.14.7, and 3.15.2, with a CVSS score of 7.6. The lower score reflects preconditions and limited scope, but the class is identical: signature verification did not guarantee that the authenticated identity was the signed one.
3. Why Libraries Keep Failing
- Two parsers, one document. Signature verification often uses a different XML stack than business logic. Any semantic difference — namespace prefix handling, entity expansion, duplicate attributes, whitespace — becomes an exploit primitive.
- Loose reference resolution. If the code searches for an assertion by ID using a broad XPath and does not require exactly one match, an attacker can add a second element with the same or a confusing ID.
- Canonicalization gaps. XML canonicalization is subtle. Comments, namespace declarations, and attribute ordering must be handled identically by signer and verifier.
- Post-validation mutation. Some frameworks deserialize the response into an object after verification and then re-read fields from the original XML. The object and the XML can diverge.
4. Testing a SAML Integration
Use a controlled lab IdP and a test service provider. Capture a valid response in an intercepting proxy, then attempt the following in order:
- Signature stripping. Remove the
<ds:Signature>element entirely. A correct SP rejects the response. - Assertion swapping. Keep the signed assertion, add a second assertion with a different
NameIDand role, and observe which one the SP honors. - Signature relocation. Move the signature into a wrapper element or into the second assertion while keeping the reference URI pointed at the first.
- Duplicate IDs. Create two assertions with the same
IDand place the malicious one first or last. - Comment and namespace tricks. Insert comments inside the
NameIDor change namespace prefixes. Some parsers normalize these away; others do not.
Tools such as SAML Raider for Burp Suite automate the common XSW variants. Treat every “signature valid” result as a question: valid over which element, and is that the element the application used?
5. Defensive Requirements
- Verify and consume the same node. Parse the document once, select the assertion, verify the signature over that exact in-memory node, and read identity attributes from that same node. Never re-parse or re-select after verification.
- Require exactly one assertion. Reject responses containing multiple assertions unless the profile explicitly supports them, and reject duplicate
IDvalues. - Enforce the full SAML profile. Validate
Audience,Recipient,InResponseTo,NotBefore,NotOnOrAfter, and the destination URL. Reject unsolicited responses unless your flow requires them. - Prevent replay. Cache assertion IDs for at least the validity window and reject reuse.
- Patch the library, then test it. Upgrade ruby-saml to 1.12.4/1.18.0 or later, samlify to 2.10.0 or later, node-saml to 5.1.0 or later, and GitHub Enterprise Server to the fixed releases. Then run the XSW test cases against your own integration; a version number is not proof of safety.
- Use a maintained XML security stack. Disable DTDs and external entities, limit document size, and reject documents with unexpected structure before signature verification.
6. Conclusion
XSW is not a parser bug in the abstract; it is a disagreement between two components about the identity of a signed element. The 2025 CVEs across Ruby, Node.js, and GitHub Enterprise Server show that the pattern survives in widely used, security-focused code. The durable fix is structural: one parse, one selected assertion, one verification, and one source of truth for identity attributes. Any architecture that validates a signature in one place and reads the user in another will eventually be vulnerable to a document that contains two different answers.