12 Aug 2026 · 32 min read

Security Assertion Markup Language (SAML) is a widely deployed standard for Single Sign-On (SSO) in the enterprise, and a recurring source of critical authentication vulnerabilities. If you have ever logged into Salesforce, Workday, an internal admin panel, or a third-party SaaS using your corporate identity, there is a good chance SAML was involved behind the scenes. It is the protocol that lets an Identity Provider (IdP) vouch for who you are, so that dozens of Service Providers (SPs) don't each have to manage passwords.

Because SAML sits directly on the authentication path, a single bug in how a Service Provider validates a SAMLResponse can mean full authentication bypass and account takeover, often as any user, including administrators. That makes SAML an attractive target for attackers and a relevant topic for pentesters, bug bounty hunters, and appsec engineers.

This guide is a practical tour of SAML vulnerabilities, from the classic signature attacks to the parser-differential and digest-confusion attacks that appeared in 2025. Each section links to a hands-on PentesterLab exercise so you can practice the attack, not just read about it.

SAML 101: How Single Sign-On Works

SAML is used to share authentication and authorization between parties. A typical SSO flow involves three actors: the User-Agent (the browser), the Service Provider (the application you want to access), and the Identity Provider (the source of truth for identity).

  1. The browser tries to access a protected resource on the Service Provider.
  2. The Service Provider redirects the browser to the Identity Provider with a SAMLRequest parameter.
  3. The Identity Provider authenticates the user and produces a SAMLResponse.
  4. The browser POSTs that SAMLResponse back to the Service Provider (usually via an auto-submitting HTML form).
  5. The Service Provider validates the response and logs the user in.

The whole trust model rests on cryptography. The Service Provider is configured ahead of time with the Identity Provider's certificate (containing its public key). When a SAMLResponse arrives signed with the matching private key, the Service Provider trusts the assertions inside it. The IdP holds the private key; everyone else only has the public key.

Anatomy of a SAMLResponse

A SAMLResponse is a Base64-encoded (and, in the redirect binding, deflated) XML document. Decoded, a simplified version looks like this:

<samlp:Response>
  <Issuer>https://idp.example.com/saml</Issuer>
  <samlp:Status><samlp:StatusCode Value="...:Success"/></samlp:Status>
  <Assertion ID="_9eb16cd0-5fd9-0135-899f-0242ac110029">
    <Issuer>https://idp.example.com/saml</Issuer>
    <ds:Signature>
      <ds:SignedInfo>
        <ds:CanonicalizationMethod Algorithm=".../xml-exc-c14n#"/>
        <ds:SignatureMethod Algorithm=".../rsa-sha256"/>
        <ds:Reference URI="#_9eb16cd0-5fd9-0135-899f-0242ac110029">
          <ds:DigestMethod Algorithm=".../sha256"/>
          <ds:DigestValue>lVqlPjCqs2NjWleua9IJzmWGyiLXl1JicDFBn5A1gZA=</ds:DigestValue>
        </ds:Reference>
      </ds:SignedInfo>
      <ds:SignatureValue>hVgoLXoClWnt7pGV20DN...</ds:SignatureValue>
      <KeyInfo><ds:X509Data><ds:X509Certificate>MIID...</ds:X509Certificate></ds:X509Data></KeyInfo>
    </ds:Signature>
    <Subject>
      <NameID Format="...:persistent">louis@example.com</NameID>
    </Subject>
    <Conditions NotBefore="..." NotOnOrAfter="...">
      <AudienceRestriction><Audience>https://sp.example.com</Audience></AudienceRestriction>
    </Conditions>
  </Assertion>
</samlp:Response>

A few elements matter enormously for security:

  • NameID: who you are. This is what an attacker usually wants to change (e.g. from louis@example.com to admin@example.com).
  • ds:Signature: the XML Digital Signature (XML-DSig) protecting the assertion. It is built from three nested pieces.
  • ds:Reference URI: a pointer (by ID) to the element that was actually signed.
  • ds:DigestValue: the hash of the referenced (canonicalized) element.
  • ds:SignatureValue: the RSA/ECDSA signature computed over the SignedInfo block.
  • Conditions / Audience: where and when the assertion is valid.

The Two Jobs of a Service Provider (and the Gap Between Them)

When a SAMLResponse arrives, the Service Provider must do two things:

  1. Verify the signature: confirm the document was signed by the trusted Identity Provider.
  2. Extract the data: read the NameID and attributes to decide who is logging in.

Almost every SAML vulnerability lives in the gap between these two steps. If the code that verifies the signature and the code that reads the identity disagree about which bytes were signed, which element to read, or even how to parse the XML, an attacker can forge an assertion the SP accepts. This is the unifying theme of every attack below.

As with JWT, a single platform often has many SAML integrations: different SPs, different libraries, different versions, different configs. Test every Assertion Consumer Service (ACS) endpoint individually. Don't assume that because the main login validates correctly, every integration does too.


Part 1: Attacking the Signature

For more than a decade, SAML attacks were almost entirely about the signature: is it checked at all, can it be removed, can the key be faked, and can the signed element be confused with an unsigned one? These bugs are still common. Understand them first.

1. Signature Not Verified

The simplest and most damaging mistake: the Service Provider reads the SAMLResponse but never verifies the signature. The signature is present, it looks legitimate, but nothing checks it.

Exploitation

  1. Start a normal SAML login and intercept the SAMLResponse in your proxy.
  2. Base64-decode it.
  3. Change the <NameID> from your email to admin@libcurl.so.
  4. Re-encode and forward it to the Service Provider.

A small decode/modify/encode helper script makes this iteration fast. If the signature isn't verified, you are now logged in as the admin.

Impact

  • Authentication bypass
  • Authorization bypass / privilege escalation

Mitigation

  • Always verify the signature before trusting any data in the assertion.
  • Use a maintained SAML library and its strict validation mode; never roll your own XML-DSig.
  • Add tests that send tampered assertions and assert they are rejected.

Practice It

👉 SAML: Introduction


2. Signature Stripping (Verified Only If Present)

A subtle variant: the Service Provider does verify the signature, but only when one is present. Remove the signature and validation is silently skipped.

Exploitation

  1. Intercept the SAMLResponse and change the NameID to admin@libcurl.so.
  2. Empty out the contents of the <ds:SignatureValue> element (or strip the whole <ds:Signature> block).
  3. Forward the modified response.

If the library treats "no signature" as "nothing to check" instead of "reject", the forged assertion is accepted.

Mitigation

  • Treat a missing or empty signature as a hard failure, not a skip.
  • Require that the expected element (the Assertion and/or the Response) is signed, and reject anything that isn't.

Practice It

👉 SAML: Signature Stripping


3. Certificate Faking (Untrusted Embedded Certificate)

SAML signatures usually embed the signing certificate inside <ds:X509Certificate>. A correct Service Provider must verify that this certificate matches the trusted IdP certificate it was configured with out of band, whether from the IdP's metadata or a pinned fingerprint. If it doesn't, it will happily verify the signature against whatever certificate the attacker supplies.

Exploitation

  1. Generate your own private key and a matching self-signed certificate.
  2. Forge a SAMLResponse for admin@libcurl.so and sign it with your private key.
  3. Embed your certificate in the response.

The signature is mathematically valid for the embedded certificate, and since the SP never checks whether that certificate is trusted, the forgery is accepted. The easiest way to produce the response is to run your own IdP (or a SAML library) using your key pair.

Mitigation

  • Trust only IdP certificates you configured out of band (from provisioned metadata or a pinned fingerprint). Never let a certificate embedded in the message, or its KeyInfo, become the trust anchor.
  • Validate the signing certificate against that configured trust store, not against itself. Handle rotation by adding the new IdP certificate to the trust store ahead of time.

Practice It

👉 SAML: Trusted Embedded Key


4. Default or Library Signing Key

Some IdP deployments use the default key shipped with a library or framework instead of generating their own. If that key is public (it's in the open-source repo, the docs, or a Docker image), anyone can sign a valid SAMLResponse.

Exploitation

  1. Identify the SAML library powering the IdP.
  2. Recover the default private key from its source or sample config.
  3. Forge and sign a SAMLResponse for the user you want to impersonate, based on the SP's SAMLRequest.

Mitigation

  • Always generate a fresh key pair per deployment; never ship or reuse sample keys.
  • Scan for known/default keys and certificates in CI.

Practice It

👉 SAML: Known Key


5. Attacker-Controlled Identity Provider

Many SaaS products let each customer organization self-configure its own IdP, uploading an IdP URL and certificate fingerprint. If the Service Provider doesn't constrain which identities that IdP is allowed to assert, an attacker can stand up their own malicious IdP and issue assertions for any account, including other tenants' admins.

Exploitation

  1. Deploy your own IdP (it can run on localhost).
  2. Configure the target SP's SAML settings for your organization with your IdP's URL and certificate fingerprint.
  3. Log in via SAML, asserting the email of a privileged user such as admin@libcurl.so, with no password required.

Mitigation

  • Bind each IdP strictly to the email domains / accounts it is authorized to assert.
  • Never let a tenant-controlled IdP assert identities outside that tenant.

Practice It

👉 SAML: Malicious IDP


6. Missing Audience / Recipient Validation (Assertion Reuse)

An assertion issued for one Service Provider should never be valid at another. The <Audience> and Recipient fields exist precisely to scope it. When a Service Provider ignores them, an attacker can take a legitimately signed assertion meant for SP #1 and replay it at SP #2.

Exploitation

  1. Tamper with the SAMLRequest from SP #1 (decode: URL-decode → Base64-decode → inflate; re-encode: deflate → Base64 → URL-encode) to change the ServiceURL.
  2. Have the IdP issue you a valid, signed assertion.
  3. Submit that assertion to SP #2, which fails to check the audience and accepts it.

Mitigation

  • Always validate Audience, Recipient, Destination, InResponseTo, and the NotBefore/NotOnOrAfter window.
  • Reject assertions not explicitly scoped to your entity ID and ACS URL.

Practice It

👉 SAML: SAMLResponse Forwarding


7. XML Signature Wrapping (XSW)

XML Signature Wrapping is the best-known SAML attack and a clear example of the "two jobs" gap. The signature covers an Assertion identified by its ID via the Reference URI. But the code that later reads the NameID may not follow that reference; it may just grab the first (or last) NameID it finds anywhere in the document.

So you keep the original, validly-signed Assertion intact (so the signature still checks out) and inject a second, unsigned Assertion containing your malicious NameID where the data-extraction logic will pick it up.

<samlp:Response>
  <!-- Unsigned, attacker-controlled, read by step #2 -->
  <Assertion ID="whatever">
    <Subject><NameID>admin@libcurl.so</NameID></Subject>
  </Assertion>
  <!-- Original signed assertion, verified by step #1 -->
  <Assertion ID="_9eb16cd0-...">
    <ds:Signature>...<ds:Reference URI="#_9eb16cd0-..."/>...</ds:Signature>
    <Subject><NameID>louis@example.org</NameID></Subject>
  </Assertion>
</samlp:Response>

Whether you put the malicious assertion first or last depends on how the target selects the NameID, so there are multiple wrapping variants to try. A notable version (CVE-2022-39299 in passport-saml, via the xmldom bug CVE-2022-39353) abuses an XML parser that accepts multiple root elements: the signed half contains no assertion at all, and the second root holds the attacker's assertion.

Mitigation

  • Always extract the identity from the exact element the signature references, never "the first/last/any" match.
  • Reject documents with multiple assertions, multiple roots, or unexpected structure (schema-validate strictly).
  • Use a SAML library that resolves the signed element by ID and operates only on it.

Practice It

👉 SAML: Signature Wrapping · SAML: Signature Wrapping II · SAML: Signature Wrapping III


8. XML Comment Injection

This classic Duo Security finding (which affected many implementations) exploits a disagreement about XML comments. Take a signed NameID and insert an XML comment:

<NameID>admin@libcurl.so<!---->.evil@attacker.com</NameID>

The signature-checking code canonicalizes the XML and ignores the comment, so the signature still validates over the full text. But the data-extraction code may use a text-node reader that stops at, or strips, the comment, returning just admin@libcurl.so. Register an account whose email becomes the victim's once everything from the comment onward is dropped, and you log in as them.

Mitigation

  • Use a comment-aware text extraction that concatenates all text nodes consistently with what was signed.
  • Upgrade libraries patched against the Duo comment-injection class of bugs.

Practice It

👉 SAML: Comment Injection · SAML: Comment Injection II


9. SSRF via Signature Reference URIs

Not every SAML bug is an auth bypass. Libraries that shell out to xmlsec1 (such as Python's pysaml2) can be coerced into Server-Side Request Forgery through the Reference URI in the signature, before the signature is even verified. If xmlsec1 isn't invoked with --enabled-reference-uris empty,same-doc, a crafted reference can make the verifier fetch an attacker-chosen URL.

Mitigation

  • Restrict reference URIs to empty,same-doc so the verifier never dereferences external resources.
  • Keep xmlsec1 and your SAML library patched, and run them with least privilege / egress filtering.

Practice It

👉 SAML: PySAML2 SSRF


10. XSLT Transform RCE

An XML Digital Signature can declare a chain of <ds:Transform> operations that are applied to the referenced element before its digest is computed. One of the transforms defined by the spec is XSLT, and the verifier is expected to run the attacker-supplied stylesheet while it processes the reference. Because that happens during reference validation, the stylesheet runs even though the signature itself never validates. If the XSLT processor exposes host-language bindings, that turns a signature check into remote code execution.

The classic case is Java. A Service Provider that verifies signatures with Apache Santuario (xmlsec for Java) and has Apache Xalan-J on the classpath runs XSLT extension functions unless secure processing is enabled. Santuario 1.4.1 never sets FEATURE_SECURE_PROCESSING, so a stylesheet can call straight into java.lang.Runtime.exec through Xalan's xalan/java bridge. This is the same flaw class as CVE-2022-47966, the Zoho ManageEngine SAML RCE.

Exploitation

  1. Take a SAMLResponse and add an XSLT transform to the signature's list of transforms.
  2. Put a stylesheet in that transform that binds the Xalan Java namespace and chains Runtime.getRuntime().exec(...) to run your command.
  3. Send the response. The transform runs as part of reference processing, so no valid signature is needed: DigestValue and SignatureValue only need to be well-formed base64, and the SP still returns 403 after your command has already executed.

Mitigation

  • Enable secure processing on every TransformerFactory (setFeature(FEATURE_SECURE_PROCESSING, true)); this disables the Java extension functions the payload relies on.
  • Allow-list transforms: a SAML assertion only needs canonicalization and enveloped-signature, so reject any other <ds:Transform> before processing it.
  • Upgrade Apache Santuario and remove Apache Xalan-J from the classpath; keep the verifier patched and least-privileged.

Practice It

👉 SAML: Transform RCE


Part 2: Attacking the Digest and the Parser

Here is a significant recent trend in SAML security, and the focus of a talk I gave on attacking SAML in 2025 (watch it on YouTube).

For years, researchers attacked the signature: is it present, is it checked, is the key trusted, is the right element signed? Defenders eventually got reasonably good at that layer. So in 2025, research moved one step deeper, from the signature to the digest and the XML parser itself.

To see why this is so powerful, expand step #1 ("verify the signature"). It is really three sub-steps:

  1. Compute a hash (digest) of the canonicalized signed element, the one containing the NameID.
  2. Compare that hash against the <DigestValue> in the message.
  3. Verify the <SignatureValue> (an RSA/ECDSA signature) over the <SignedInfo> block, using the public key.

The whole scheme is only safe if every step sees the same bytes: the bytes that get hashed must be the bytes that get read, and the digest that is compared must be the digest that is signed. The 2025 wave of attacks breaks exactly this assumption, not by forging a signature, but by making different parts of the pipeline parse the same document differently. This bug class is called a parser differential.

11. Digest Comment Injection: "SAMLStorm" (CVE-2025-29775)

This is a clear illustration of attacking the digest layer. In the Node.js library xml-crypto (a widely used XML-signature library that many SAML stacks depend on), the <DigestValue> was read inconsistently when it contained an XML comment.

Specifically, when comparing the computed hash against DigestValue, the library used the value inside the comment, but when feeding the digest into signature verification it used the value after the comment. That lets an attacker satisfy both checks at once with a single element:

<DigestValue><!--MALICIOUSHASH-->LEGITIMATEHASH</DigestValue>

Now you can tamper freely with the assertion:

  1. Save the original document's legitimate digest: LEGITIMATEHASH.
  2. Change the NameID to admin@libcurl.so.
  3. Compute the new digest of your modified assertion: MALICIOUSHASH.
  4. Set the digest to <DigestValue><!--MALICIOUSHASH-->LEGITIMATEHASH</DigestValue>.

The comparison step sees MALICIOUSHASH (which matches your tampered assertion), while the signature-verification step still sees LEGITIMATEHASH (which matches the original, untouched signature). Both pass. The hard part of this challenge is simply building tooling to canonicalize and hash a document correctly; once you can reproduce the IdP's own digest, exploitation is trivial. A follow-up challenge pushes it further: forge a response from scratch using a signed blob pulled from the IdP's published metadata, even when you don't have a valid SAMLResponse to start from.

Mitigation

  • Upgrade xml-crypto (≥ 6.0.1 / 3.2.1 / 2.1.6) and any SAML library that depends on it.
  • Reject comments inside signature-critical nodes; normalize text consistently across compare and verify.

Practice It

👉 SAML: CVE-2025-29775 · SAML: CVE-2025-29775 Signed Metadata


12. Parser Differentials: ruby-saml (CVE-2025-25291 / CVE-2025-25292)

One notable 2025 result targeted ruby-saml. The library parses a SAMLResponse with two different XML parsers, REXML and Nokogiri, and they don't always agree. Worse, they don't even receive the same input: REXML parses the raw response, while Nokogiri parses the result of calling to_s on the document REXML built.

By abusing DOCTYPE declarations, CDATA, and comments, an attacker can craft a single document containing two samlp:Response elements where one parser sees the legitimate NameID (so the signature validates) and the other extracts the malicious NameID (so you log in as someone else):

<!DOCTYPE foo SYSTEM 'x" [<!ATTLIST ...>]><!-- '>
<samlp:Response>HACK<![CDATA[-->
<samlp:Response>WACK<!--]]>--></samlp:Response>

Run through REXML vs. Nokogiri, that input yields different "active" responses, the defining signature of a parser differential. As the original write-up put it, this lets you "sign in as anyone." With a single valid signature, an attacker can construct assertions for any user and achieve full account takeover. This is the work I cover in the talk linked above, and it is why parser differentials, not signature forgery, are where a lot of current SSO security research is focused.

Mitigation

  • Upgrade ruby-saml to a patched release (≥ 1.18.0) and watch for follow-up advisories.
  • Parse with a single hardened parser; never verify on one representation and read from another.
  • Disable DOCTYPE/DTD processing and reject documents with more than one root or response element.

Practice It

👉 SAML: CVE-2025-25291


The Bigger Picture: Parser Differentials Are a Bug Class

Once you start seeing SAML through the parser-differential lens, you see the same shape everywhere: HTTP request smuggling, path-traversal via URL normalization, and authentication-middleware bypasses all come from two components disagreeing about the same input. The same root cause that breaks SAML digest verification also breaks fast/native URL parsers that route a request to a protected handler while telling the auth guard the path is harmless.


A SAML Hardening Checklist

If you are building or reviewing a Service Provider, treat each of these as a test case:

  • Always verify the signature before reading any data, and fail closed if it is missing or empty.
  • Verify that the signature covers the exact element you read the identity from (resolve by reference, not "first/last/any").
  • Establish IdP certificate trust out of band (configured metadata or pinned fingerprint); never trust a certificate embedded in the message.
  • Validate Audience, Recipient, Destination, InResponseTo, and the validity window.
  • Parse with a single hardened XML parser; disable DTD/DOCTYPE; reject multiple roots, multiple assertions, and comments in signed nodes.
  • Bound message size after inflation to prevent decompression bombs.
  • Bind tenant-configured IdPs to the identities they may assert.
  • Keep your SAML library and its XML/crypto dependencies patched and pinned.

Final Thoughts: From Signature to Parser

SAML's attack surface has shifted. The classic signature attacks (missing verification, stripping, wrapping, certificate faking) are still common and still worth learning. But the 2025 wave of digest-confusion and parser-differential bugs (SAMLStorm in xml-crypto, the parser differentials in ruby-saml) shows that the most dangerous modern flaws live below the signature, in the assumption that every component parses the same bytes the same way.

If you're pentesting an app: test every ACS endpoint, look for disagreements between "what was signed" and "what is read," and probe the parser: comments, DOCTYPE, CDATA, and duplicate elements are worth probing.

If you're a developer or security engineer: never trust attacker-controlled XML structure, verify and read from the same representation, and keep your XML and crypto dependencies current.

Reading Isn't Enough: You Have to Practice

Understanding a SAML attack in theory is one thing; pulling it off (correctly canonicalizing XML, recomputing a digest, and threading a forged assertion past validation) is another. That's why every section above links to a hands-on PentesterLab exercise built from real CVEs and real-world bugs.

PentesterLab teaches web security through hands-on exercises based on real bugs, real CVEs, and real-world applications, and is used by red teams, appsec teams, and security researchers to build practical skills.

👉 Start practicing on PentesterLab and build your SAML and SSO exploitation skills.

Want to build these skills hands-on?

PentesterLab has 700+ real-world labs on web hacking, code review, and vulnerability analysis. Start with a free account.

Photo of Louis Nyffenegger
Louis Nyffenegger
Founder and CEO @PentesterLab