CONCEPT Cited by 1 source
XML signature wrapping¶
XML Signature Wrapping (XSW) is a family of attacks against XML-DSig verification in which the signed bytes and the interpreted bytes diverge. A verifier confirms that a valid signature exists somewhere in the document — and then consumes data from somewhere else in the same document, trusting it as if the signature covered it. Because XML-DSig allows signatures to reference signed content by URI / XPath rather than by byte range, there's always a gap between locating the signed element and locating the consumed element — and that gap is where XSW lives.
The attack was first documented by McIntosh & Austel in 2005 and has re-appeared across XML-DSig implementations ever since (including against SAML SSO, WS-Security, and XMLDSig-signed SOAP).
The gap XSW exploits¶
XML-DSig verification has three logically-distinct byte ranges:
- Signature bytes — the
<ds:SignatureValue>contents. - Signed-info bytes — the canonicalised
<ds:SignedInfo>element that the SignatureValue is computed over. - Referenced-element bytes — the
<Assertion>(or other element pointed at by<ds:Reference URI="...">) whose canonicalised hash must equal the<ds:DigestValue>inside<ds:SignedInfo>.
A correct implementation links all three: verifying (1) against (2) proves the IdP signed (2); then the digest inside (2) proves (3)'s canonicalised bytes match. XSW attacks the link — usually between (2) and (3), sometimes between (1) and (2).
Classic XSW variants¶
- Simple wrapping — inject a duplicate
<Assertion>with the attacker's claims; the verifier consumes the attacker's copy while the signature check runs against the original. (Mitigation that ruby-saml 1.17 had: require exactly one element with a givenID.) - Comment / whitespace injection — some canonicalisers treat
comments / whitespace differently from document-order traversal,
allowing a
<Subject>admin<!---->@attacker.com</Subject>to canonicalise asadminto one pass andadmin@attacker.comto another. - Roundtrip attacks — publishing one XML representation that serialises differently after parse-and-re-serialise in ways that matter for signature vs data extraction. (Forsén 2021, Mattermost.)
- Parser-differential wrapping — the exploited element seen by parser A is not the same element seen by parser B. The two parsers each return a plausible answer and all checks pass, but each check ran against a different part of the document.
Parser-differential flavour (ruby-saml 2025)¶
This is the class the ruby-saml disclosure belongs to:
SAML response contains:
<Assertion ID="attacker"> ← fabricated, contents = "admin" user
<Subject>admin</Subject>
</Assertion>
<StatusDetail>
<Signature> ← visible only to Nokogiri
<SignedInfo>
<DigestValue>hash(fabricated assertion)</DigestValue>
</SignedInfo>
<SignatureValue>junk</SignatureValue>
</Signature>
</StatusDetail>
<Signature> ← visible to both parsers
<SignedInfo>
<DigestValue>hash(real, benign assertion)</DigestValue>
</SignedInfo>
<SignatureValue>[valid IdP signature]</SignatureValue>
</Signature>
ruby-saml locates <SignedInfo> with Nokogiri
(gets the <StatusDetail> one — attacker's) and canonicalises it
against the <SignatureValue> extracted with REXML
(gets the outer one — valid). The signature check passes: valid IdP
signature, canonicalised SignedInfo from a Signature, cryptography
is happy. The digest check then reads <DigestValue> with REXML (gets
the outer one), hashes the referenced assertion with Nokogiri (hashes
the attacker's fabricated one — because its ID matches the outer
Reference), and the digest-value extracted and the hash-computed
mismatch — unless the attacker arranges the ID routing so the
canonicalised attacker assertion exactly matches the legitimate outer
<DigestValue>, OR arranges two Signatures such that the outer
DigestValue matches the attacker assertion's hash. The ruby-saml
disclosure shows the second shape: "The assertion is retrieved via
Nokogiri by looking for its ID. This assertion is then canonicalized
and hashed... The hash is then compared to the hash contained in the
DigestValue. This DigestValue was retrieved via REXML. This
DigestValue has no corresponding signature."
Generalised structural fix¶
From the ruby-saml post, architecture-level:
"If the library had used the content of the already extracted
SignedInfoto obtain the digest value, it would have been secure in this case even with two XML parsers in use."
Once the bytes of <SignedInfo> are cryptographically verified,
every subsequent extraction — the DigestValue, the Reference URI,
the transforms — must come from those bytes, not from re-querying
the document. This pins the verification chain together and
eliminates the gap XSW exploits regardless of the underlying parser
differentials.
Seen in¶
- sources/2025-03-15-github-sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials — canonical 2025 parser-differential XSW instance against systems/ruby-saml (CVE-2025-25291, CVE-2025-25292), fixed in ruby-saml 1.18.0. Exploitable instance also confirmed in systems/gitlab. Two independent researchers found two different parser-differentials each yielding a bypass.
Related¶
- concepts/parser-differential — the underlying mechanism for this variant.
- concepts/canonicalization-xml — the XML-DSig primitive that allows multiple byte-level representations of the same logical element and is another frequent XSW substrate.
- concepts/saml-authentication-bypass — the canonical outcome category when XSW lands on SAML SSO.
- systems/saml-protocol — the spec that mandates the signature structure XSW attacks.
- patterns/single-parser-for-security-boundaries — the structural prevention.