Skip to content

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:

  1. Signature bytes — the <ds:SignatureValue> contents.
  2. Signed-info bytes — the canonicalised <ds:SignedInfo> element that the SignatureValue is computed over.
  3. 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 given ID.)
  • Comment / whitespace injection — some canonicalisers treat comments / whitespace differently from document-order traversal, allowing a <Subject>admin<!---->@attacker.com</Subject> to canonicalise as admin to one pass and admin@attacker.com to 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 SignedInfo to 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

Last updated · 200 distilled / 1,178 read