Skip to main content
All articles

SAML signature wrapping and Golden SAML: a valid signature on the wrong assertion

A SAML response can carry a genuine, valid signature and still be a forgery, because the service checks that a signature exists rather than that it covers the assertion it trusts. That gap is signature wrapping. Golden SAML is its post-compromise cousin. Here are the real CVEs, a lab, and the verification logic that closes both.

By Published

9 min read0 views

Single sign-on rests on one act of faith: the service provider believes an XML document because an identity provider signed it. The entire security of the flow reduces to one question the service provider must answer correctly on every login: is the assertion I am about to trust the exact bytes the IdP signed? Signature wrapping is what happens when the answer is "there is a valid signature somewhere in this document" instead of "yes". Those are not the same sentence, and the difference is an authentication bypass.

The one distinction

The naive verifier asks: does this response contain a signature that validates against the IdP's key? The correct verifier asks: is the specific assertion I extract and act on the one that this valid signature covers?

Signature wrapping exploits the space between those two questions. The attacker takes a legitimately signed assertion, wraps or relocates it inside the XML, and adds a second assertion, unsigned and attacker-authored, positioned where the service provider's code will read it. Signature verification passes, because the original signed element is still present and still valid. Then the application logic selects a different element, the malicious one, and logs the attacker in as whoever it names. The signature was real. The trust was misplaced.

Golden SAML is the same outcome by a different route, and the pair is worth holding apart:

  • Signature wrapping is a bug in the service provider. The IdP is honest, the signature is genuine, and the SP's verification is fooled into consuming an element the signature does not cover. You fix it by fixing the verifier.
  • Golden SAML is a post-compromise technique against the identity provider. An attacker who has stolen the IdP's token-signing private key (for example, the ADFS signing certificate) forges assertions that are legitimately signed. There is no verification bug to find, because the signatures are valid by construction. You cannot fix it at the SP; you contain it by protecting and rotating the signing key.

One is a coding flaw you patch. The other is a trust catastrophe you detect and rebuild from. They share a symptom, "authenticated as an arbitrary user", and nothing else.

Why it varies: XML makes "what was signed" ambiguous

XML Signature is unusually hard to verify safely, and the reason is structural. A signature references what it covers by an ID or an XPath, the document can be rearranged without breaking well-formedness, and two XML parsers can disagree about which element a given path selects. Academic work established this a decade ago: the USENIX Security paper "On Breaking SAML: Be Whoever You Want to Be" by Somorovsky and colleagues found XML signature wrapping flaws in 11 of 14 major SAML frameworks examined. The takeaway was not that one library was bad. It was that verifying an XML signature and consuming the signed element is a two-part obligation, and libraries kept doing the first part while leaving the second to the caller.

The variance you meet in the field is:

  • Verify the signature, then re-parse the document and read "the assertion", getting a different node than the one verified.
  • Trust the first <Assertion> when a signature covers the second, or vice versa.
  • Two parsers in one stack (one to verify, one to read) that select different nodes for the same input.

The real CVEs

The anchor: CVE-2024-45409. NVD rates it critical (CVSS 9.8; the ruby-saml advisory GHSA-jw9c-mfg7-9rx2 rates it 10.0), CWE-347, "Improper Verification of Cryptographic Signature". The description is stark: ruby-saml in the affected ranges "does not properly verify the signature of the SAML Response," so "an unauthenticated attacker with access to any signed SAML document (by the IdP) can thus forge a SAML Response/Assertion with arbitrary contents" and "log in as arbitrary user." One signed document from the IdP, of any kind, was enough raw material to mint a response for anyone. It is fixed in ruby-saml 1.17.0 and 1.12.3.

The sequel that proves the class is deep: CVE-2025-25291 and CVE-2025-25292. GitHub Security Lab's write-up, "Sign in as anyone: bypassing SAML SSO authentication with parser differentials," describes a bypass in ruby-saml up to 1.17.0 where an attacker with a single valid signature exploited the fact that two XML parsers, REXML and Nokogiri, interpret the same document differently. One parser verified the signature over one element; the other handed the application a different element. That is signature wrapping expressed as a parser differential, and it landed after the 1.17.0 fix for CVE-2024-45409, which is why "we patched the SAML library once" is not a closed matter.

Golden SAML in the record. The technique was named by CyberArk research and is documented by CISA in advisory AA21-008A, "Detecting Post-Compromise Threat Activity in Microsoft Cloud Environments," which discusses forged SAML tokens and attackers exporting token-signing certificates from ADFS. That advisory is the detection reference for the case where the signatures are valid because the attacker holds the key.

A lab you can build

A Linux VM and Python are enough. No Docker, and no live IdP: the point is to reproduce the verifier's mistake, not to attack anyone.

python3 -m venv samllab && . samllab/bin/activate
pip install lxml

The lab is a naive verifier and a wrapped document. It shows the two-question gap without shipping anything that works against a real service.

# samllab.py: teaching harness that demonstrates the verify then consume gap
from lxml import etree

# Stand-in for real crypto: pretend any <Assertion id="signed-real"> is the
# element the IdP signature covers. The bug we are showing is NOT weak crypto;
# it is reading a different element than the verified one.
def signature_is_valid(doc):
    signed = doc.xpath('//Assertion[@id="signed-real"]')
    return len(signed) == 1  # the genuine signed assertion is present

def naive_consume(doc):
    # BUG: read "the first assertion" rather than the signed one
    return doc.xpath('//Assertion')[0].get("user")

# A response the attacker built: genuine signed assertion kept intact,
# a forged one prepended where naive code reads first.
wrapped = etree.fromstring(b"""
<Response>
  <Assertion id="forged" user="admin"/>
  <Assertion id="signed-real" user="alice"/>
</Response>""")

print("signature valid?", signature_is_valid(wrapped))  # True
print("logged in as   :", naive_consume(wrapped))        # admin

Run python samllab.py. The signature check passes on the genuine element, and the application logs in as admin from the forged one. Now fix the consume step so it can only read the element the signature covered, and reject a document that carries more assertions than were signed:

def safe_consume(doc):
    assertions = doc.xpath('//Assertion')
    if len(assertions) != 1:
        raise ValueError("unexpected assertion count")
    node = assertions[0]
    if node.get("id") != "signed-real":   # must be the verified element
        raise ValueError("consumed assertion is not the signed one")
    return node.get("user")

Against the wrapped document, safe_consume raises instead of returning admin. The lab is deliberately abstract about the cryptography, because the flaw is never in the hash function. It is in trusting an element other than the one the signature bound.

Finding it in source

  • A verify call and a separate read. The dangerous pattern is verify(response) followed by response.find("Assertion") or an XPath that re-selects from the whole document. If the code that consumes the assertion is not handed the exact node that was verified, it can drift to another one.
  • Two XML parsers in one path. Grep the SAML code for more than one of lxml, REXML, Nokogiri, xml.etree, DocumentBuilder. A verifier and a reader that are different libraries is the CVE-2025-2529x parser-differential shape.
  • "First assertion" assumptions. assertions[0], getElementsByTagName("Assertion").item(0), //saml:Assertion[1]. Any positional selection assumes the signed element is where you expect, which is precisely what wrapping breaks.
  • Old SAML library versions. Check the lockfile. ruby-saml below 1.17.0 or 1.12.3 is CVE-2024-45409; at or below 1.17.0 with both REXML and Nokogiri present is exposed to the parser-differential follow-ups. Pin to fixed releases.

Fixing it

  • Update the library and keep updating it. ruby-saml 1.17.0/1.12.3 for CVE-2024-45409, and the later fixes for the parser-differential CVEs. This class reopened once already; treat SAML library currency as ongoing, not one-off.
  • Consume only the verified element. The node your application reads must be the identical node the signature covered, threaded through from the verification step, not re-selected from the document afterwards.
  • Reject structural surprises. A response with more than one assertion, or an assertion in an unexpected position, is refused rather than reconciled. Do not let the parser pick a winner.
  • For Golden SAML, protect the key. There is no SP-side patch. Store the ADFS or IdP token-signing key in an HSM, restrict and monitor who can export it, and rotate it (twice, to fully retire the old key) on any suspicion of domain compromise.

Detection

  • At the service provider, log the ID and position of the assertion consumed against the assertion verified. A mismatch, or a response carrying multiple assertions, is the wrapping signature.
  • At the identity provider, follow CISA AA21-008A: watch for exports of the token-signing certificate and for service-provider logins that have no corresponding authentication event at the IdP. A valid assertion for a session the IdP never issued is the Golden SAML tell, and it is the only tell, because the signature itself is genuine.
  • Alert on library drift. A SAML dependency that falls behind the fixed versions is a detectable, standing exposure; treat the version check as a control.

Take this away

A SAML signature being valid tells you an element was signed by the IdP. It does not tell you that the element you are about to trust is that one. Signature wrapping lives in the gap between those statements, and you close it in the service provider by consuming only the verified node and refusing structural surprises. Golden SAML closes that gap from the attacker's side by stealing the signing key, so its signatures are honest and only the identity provider's own records can catch it. When you review an SSO integration, do not stop at "we verify the signature." Follow the assertion from verification to use and confirm they are the same bytes.

Sources

Was this useful?

Share

Tags

  • web security
  • cryptography
  • active directory
  • detection engineering
  • cve analysis

Get the next write-up by email

New technical write-ups by email when we publish. No sales sequence, and every email has an unsubscribe link.

Prefer a feed? RSS. How we handle your address: privacy notice.

Comments

Loading comments…

Leave a comment

Comments are read and approved by hand before they appear, so yours will not show up straight away. Your email address is optional, is never published, and is only used if we need to reply to you directly.

0/5000

Related service

Internal & External Penetration Testing

Find the paths into your network, and the paths across it once someone is in.

If you want to know whether what you have just read applies to your own systems, that is the engagement that answers it.