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 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).
SAMLRequest parameter.SAMLResponse.SAMLResponse back to the Service Provider (usually via an auto-submitting HTML form).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.
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.When a SAMLResponse arrives, the Service Provider must do two things:
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.
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.
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.
SAMLResponse in your proxy.<NameID> from your email to admin@libcurl.so.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.
A subtle variant: the Service Provider does verify the signature, but only when one is present. Remove the signature and validation is silently skipped.
SAMLResponse and change the NameID to admin@libcurl.so.<ds:SignatureValue> element (or strip the whole <ds:Signature> block).If the library treats "no signature" as "nothing to check" instead of "reject", the forged assertion is accepted.
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.
SAMLResponse for admin@libcurl.so and sign it with your private key.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.
KeyInfo, become the trust anchor.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.
SAMLResponse for the user you want to impersonate, based on the SP's SAMLRequest.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.
admin@libcurl.so, with no password required.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.
SAMLRequest from SP #1 (decode: URL-decode → Base64-decode → inflate; re-encode: deflate → Base64 → URL-encode) to change the ServiceURL.Audience, Recipient, Destination, InResponseTo, and the NotBefore/NotOnOrAfter window.👉 SAML: SAMLResponse Forwarding
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.
👉 SAML: Signature Wrapping · SAML: Signature Wrapping II · SAML: Signature Wrapping III
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.
👉 SAML: Comment Injection · SAML: Comment Injection II
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.
empty,same-doc so the verifier never dereferences external resources.xmlsec1 and your SAML library patched, and run them with least privilege / egress filtering.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.
SAMLResponse and add an XSLT transform to the signature's list of transforms.Runtime.getRuntime().exec(...) to run your command.DigestValue and SignatureValue only need to be well-formed base64, and the SP still returns 403 after your command has already executed.TransformerFactory (setFeature(FEATURE_SECURE_PROCESSING, true)); this disables the Java extension functions the payload relies on.<ds:Transform> before processing it.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:
NameID.<DigestValue> in the message.<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.
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:
LEGITIMATEHASH.NameID to admin@libcurl.so.MALICIOUSHASH.<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.
xml-crypto (≥ 6.0.1 / 3.2.1 / 2.1.6) and any SAML library that depends on it.👉 SAML: CVE-2025-29775 · SAML: CVE-2025-29775 Signed Metadata
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.
ruby-saml to a patched release (≥ 1.18.0) and watch for follow-up advisories.DOCTYPE/DTD processing and reject documents with more than one root or response element.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.
If you are building or reviewing a Service Provider, treat each of these as a test case:
Audience, Recipient, Destination, InResponseTo, and the validity window.DOCTYPE; reject multiple roots, multiple assertions, and comments in signed nodes.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.
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.