Signature wrapping rides again: duplicate-ID XML::Sig and the SAML assertion you did not sign

XML signature wrapping is one of those vulnerability classes that never really leaves. Every few years it resurfaces in a new library, wearing a slightly different disguise, and every time the fix is the same idea that the previous library also failed to apply. The latest instance lives in XML::Sig for Perl, in versions before 0.71, and it is worth walking through carefully because the mechanics explain a whole family of single sign-on breaks that show up in our engagements.

The one-line version

XML::Sig verifies a signed document, reports success, and hands the application a document whose security-relevant contents were never actually covered by the signature it checked. In a SAML2 deployment that means an attacker can present an assertion that validates cleanly while the identity, attributes, and authorization claims the application reads are attacker controlled.

Where the check goes wrong

When XML::Sig validates a document, it walks the SignedInfo block, finds each Reference, and resolves the reference URI back to the element that the digest is supposed to cover. In the affected versions, _get_signed_xml() in lib/XML/Sig.pm resolves that reference with the XPath expression //*[@ID='$id'] and returns the first node of the resulting node set.

Read that expression again, because the whole bug is contained in it. //*[@ID='$id'] says "find every element anywhere in the document whose ID attribute equals this value." XPath is perfectly happy to return more than one match. A well formed document is supposed to have unique IDs, but nothing in this code path enforces that. XML::Sig takes the first node in document order, verifies the digest and signature against it, and moves on. The second element carrying the same ID is never examined and never flagged.

Turning a parsing quirk into an authentication bypass

Now put an application on the other side of that verification. A SAML service provider does not re-derive trust from the signature library's internal node handle. It re-parses the response and looks up the assertion by its ID, or by walking the document tree, using its own code. If the attacker constructs a response that contains two elements sharing the signed ID value, the library validates the first one while the application reads the second.

The attack shape is textbook signature wrapping:

  1. Start from a legitimately signed assertion captured or replayed from an earlier flow. Its digest and signature are valid for its original contents.
  2. Inject a second element carrying the same ID attribute value, positioned so that the application's own lookup resolves to it rather than to the signed original.
  3. Populate that second element with the identity and attributes you want: a different NameID, an elevated group, an administrator email.
  4. Submit. XML::Sig checks the first element, the signature passes, and the service provider consumes the attacker's element as authenticated fact.

The signature is real. The math is correct. The problem is that the bytes the library vouched for and the bytes the application trusted are not the same bytes.

Why this keeps happening

Signature wrapping survives because XML signatures cover a reference to content, not the content the consumer will actually use. The security property everyone assumes ("this document is signed") is not the property the library provides ("some element that once matched this ID hashed correctly"). Any gap between the node the verifier pins and the node the application reads becomes an authentication bypass. Duplicate IDs are the simplest way to open that gap, but relocation, comment splitting, and detached-signature tricks all exploit the same mismatch.

The correct behavior is to reject any document where the reference resolves ambiguously, to verify against the exact node the application will consume, and to fail closed when ID uniqueness cannot be guaranteed. XML::Sig 0.71 tightens the resolution so the wrapped duplicate is no longer silently accepted.

How to tell if you are exposed

The fast check is a version probe: if any service in your estate uses XML::Sig below 0.71 for SAML or any other signed-XML flow, treat it as vulnerable and prioritize the upgrade. The higher-value check is architectural. Anywhere you consume signed XML, confirm that the component reading the security claims is reading the same node the signature verifier pinned, not doing an independent lookup by ID or tag name. That second, independent lookup is the recurring root cause across every signature-wrapping CVE we have triaged, regardless of language.

What to do this week

Upgrade XML::Sig to 0.71 or later on every host that verifies signed XML. If you cannot patch immediately, put schema validation with strict ID-uniqueness enforcement in front of the verifier, and reject any response whose ID values are not unique before it reaches the signing check. Then audit your single sign-on logs for assertions that carry duplicate ID attributes, because a spike there is the clearest signal that someone has already found this.

We track this issue in the CELVEX scanner catalog and test for the duplicate-ID acceptance pattern directly rather than relying on a version banner alone, since the same wrapping logic reappears in forks and vendored copies that never advertise a version at all.

Reference