⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
⚠ Thin coverage — only 5 disclosed reports for this class; illustrative, not exhaustive.
§Basic information
SAML is XML-based single sign-on. An Identity Provider (IdP) issues a digitally-signed <Response> containing an <Assertion> that states who you are (the NameID plus attributes), and a Service Provider (SP) grants you a session if the XML-DSig signature validates. The whole trust model collapses to one question: does the signature you validated actually cover the identity you consumed? SPs answer that wrong constantly.
A SAML break is not a low-severity crypto nit — it is a pre-auth, no-credentials account takeover of any user, including a site admin. The signature is checked once, then the SP walks the XML to pull out a NameID; anywhere the validated node and the consumed node can differ, you log in as anyone. Treat every SAML SP as an ATO surface, not a login button.
§Methodology
Capture the ACS POST. Log in via SSO with Burp on and grab the request to the Assertion Consumer Service — POST /users/saml/auth, /saml/acs, /sso/saml, etc. It carries SAMLResponse (base64, sometimes deflated) plus RelayState.
Decode and read the XML. Base64-decode (inflate if redirect-binding) and pretty-print. Locate three things: where the <Signature> sits, what it References (URI="#_id"), and where the NameID/attributes the SP reads actually live.
Ask if validated ≠ consumed. If the signature covers one node but the SP reads the first <Response>/<Assertion> it finds, you have signature wrapping.
Probe each independent bug below with the captured response as your template.
Forge the identity — swap the NameID to admin@TARGET and confirm you land in the victim's session.
# Decode a POST-binding SAMLResponse and inspect the DSig structure
echo 'BASE64_SAMLRESPONSE' | base64 -d | xmllint --format -
# Note: <Signature> Reference URI="#_id" vs where <NameID>/<Attribute> live
● NOTE
Load the captured response into SAML Raider (Burp extension) — it clones, edits, and re-signs responses and automates the XSW permutations so you are not hand-editing XML in Repeater.
§Technique variants
Each of these is an independent break; test all of them against any SP.
XML Signature Wrapping (XSW)
The core primitive. The SP validates the first <Signature> it finds anywhere, then reads assertions from the first <Response>/<Assertion> it finds anywhere, and never checks the two are the same node. Take a validly-signed response and prepend an unsigned, attacker-controlled one — the parser hands the SP index [0] (yours), while the signature check passes on the original element further down.
<!-- assemble this XML, then base64-encode it (deflate first for redirect binding)
into the SAMLResponse form field before POSTing to the ACS -->
<samlp:Response> <!-- unsigned, attacker-controlled, parsed as [0] -->
<Assertion><Subject><NameID>admin@TARGET</NameID></Subject></Assertion>
</samlp:Response>
<samlp:Response> <!-- original, validly signed - keep intact -->
<Assertion>...</Assertion><ds:Signature>...</ds:Signature>
</samlp:Response>
The vulnerable pattern is hard-coded [0] indexing that decouples the signature target from the data target:
// signature validated on: xpath(doc,"//*[local-name()='Signature']")[0]
// identity read from: doc.getElementsByTagNameNS('...SAML:2.0:protocol','Response')[0]
// two different [0] nodes -> validate one, consume the other
XSW has many placements (wrap the malicious assertion inside the signed one, in the <Extensions>, as a sibling, etc.) — SAML Raider cycles all of them.
Signature reuse from public metadata
XSW usually needs one valid login to harvest a signed response. You often do not even need that: IdP federation metadata XML is frequently public and signed. Wrap your attacker assertion around that already-valid signature and POST it straight to the ACS — no legitimate session, no key theft, no cracking.
# The signed metadata is a free valid signature to wrap
curl -s https://IDP/federationmetadata/2007-06/FederationMetadata.xml -o meta.xml
# Extract <ds:Signature>, wrap attacker <Assertion>(NameID=admin) around it, POST to the ACS
Signature-optional / fail-open verification
Verifiers that return early when no cert/key is configured turn a misconfiguration into a full auth bypass. If you can flip the SP into a cert-less state yourself — e.g. via a client-callable config RPC with no auth check — signature verification no-ops and you submit an entirely unsigned assertion.
// On a SAML-enabled login page, unauthenticated RPC that mutates SAML settings:
Meteor.call("addSamlService", "Default_cert") // registers a provider whose cert is falsy
// verifySignatures() now no-ops -> POST a hand-crafted UNSIGNED SAML response asserting an admin NameID
Even without a config RPC, always send an unsigned assertion once — some SPs accept it outright.
Assertion replay (no single-use)
If the SP validates the signature but never records consumed assertion IDs, a captured SAMLResponse is a reusable session key until its NotOnOrAfter (which the IdP TTL can set to hours or days). Any assertion you steal via MITM, XSS, or phishing mints fresh sessions on every POST.
POST /users/saml/auth HTTP/1.1
Host: TARGET
Content-Type: application/x-www-form-urlencoded
SAMLResponse=<captured_base64_response>&RelayState=...
# Replay in Repeater N times -> a NEW valid session each time = no assertion-ID cache
entityId / issuer normalization desync
The org/tenant a user is provisioned into is often keyed off the IdP entityId (issuer). When the validation step normalizes (trim/case/unicode) but the lookup/provision step does not, a near-duplicate identifier collides: register a tenant with victimEntityId plus a trailing space, and legit users authenticate against the real IdP yet get provisioned into your org.
# Validation compares against trim(issuer); provisioning keys on the raw, space-suffixed value
Legit entityId: urn:victim:idp
Attacker entityId: "urn:victim:idp " # trailing space -> wins provisioning, survives trim() check
# Test near-duplicates differing only by whitespace / case / trailing dot
§Bypasses
Filter / control
Bypass
Seen in
Signature validated but node not pinned
XML Signature Wrapping — prepend unsigned <Response>; SP validates sig on one [0] node, reads identity from another
#812064
Need a valid signature to wrap
Reuse the signature from the IdP's public signed federation metadata — no login, no key theft
#2579939
Signature verification enabled
Flip SP cert-less via unauth config RPC → verifySignatures() returns early → unsigned assertion accepted
#1049375
entityId validated with trim()
Provision keyed to the untrimmed, space-suffixed value → cross-tenant collision + SSO DoS
#976603
Signature valid + assertion in-window
No consumed-assertion cache → re-POST the same SAMLResponse for a new session each time
#888930
▲ WARNING
"The signature validated" is not proof of security. XSW passes signature validation by design — the exploit lives in the gap between which node was signed and which node is read. Always confirm the SP pins identity extraction to the signed reference, not to getElementsByTagName(...)[0].
§Escalation & impact
Every SAML break in this corpus ends in ATO or full compromise — see the chains page:
XSW → site-admin → full compromise. Forge a NameID of an admin user and provision straight into the highest-privilege account (#2579939 GHES, CVE-2024-6800; #812064 Rocket.Chat).
Unauth RPC → cert-less provider → forged SAML → admin. A client-callable config method chains into a complete signature bypass (#1049375).
entityId collision → SSO DoS → cross-tenant provisioning → ATO. The space-suffixed tenant wins provisioning; migrated users' accounts are inherited after a rename (#976603).
Stolen assertion → durable sessions. MITM/XSS/phishing exfil of one SAMLResponse becomes reusable auth when single-use isn't enforced (#888930).
Because SAML sits in front of the entire app, a break is equivalent to owning the auth server — pair it with any IdP-trusting downstream (SCIM provisioning, admin panels) for lateral movement. Related surfaces: oauth, jwt.
§Prevention
Pin the signature reference to the consumed node. The SP must confirm the validated <Signature> References the exact<Assertion>/<Response> it reads identity from; reject documents containing multiple Responses/Assertions; use a schema-hardened, ID-anchored verifier. This kills XSW.
Fail closed on missing cert/key. Signature verification must be mandatory — never return true or no-op when no cert is configured. Config-mutating RPC must require admin auth.
Normalize identically at every stage. Apply the same trim/case/unicode normalization to entityId/issuer for both validation and provisioning, and forbid near-duplicate tenant identifiers.
Validate the rest of the envelope. Check <Status>after signature, and verify Destination, Audience, and InResponseTo. Pin the trusted IdP certificate — never trust a cert embedded in the response.
The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 5 in this class.
Rocket.Chat exposes the addSamlProvider/addSamlService Meteor method to unauthenticated clients with a user-controlled provider name; verifySignatures() returns early when no cert is configured, so adding a provider with no cert makes the server accept an arbitrary forged SAML response and log in as any user (including admin).
Method
On the login page of a SAML-enabled Rocket.Chat (default provider name 'Default'), open a JS console with a Meteor connection.
Call Meteor.call('addSamlService','Default_cert') to register a provider whose cert setting is falsy.
Because verifySignatures() no-ops without a cert, submit a self-crafted (unsigned) SAML assertion for any username to authenticate as that user.
Meteor.call("addSamlService", "Default_cert")
// then POST a hand-crafted SAML response asserting an admin NameID; no signature required
Insight — Enumerate exposed Meteor/DDP methods (and any RPC framework's client-callable methods) for ones that mutate security settings without an auth/permission check. Also: SAML/OIDC verifiers that 'return early' when a cert/secret is absent turn misconfiguration into full auth bypass.
GHES SAML auth with IdPs that publish signed federation metadata XML was vulnerable to XML signature wrapping (XSW): an attacker with network access could reuse the exposed valid signature to forge a SAML response and provision/log in as a site admin without prior auth (CVE-2024-6800).
Method
Obtain the IdP's publicly exposed signed federation metadata XML
Construct a SAML response embedding the valid signature but wrapping attacker-controlled assertion (XSW)
Submit to the GHES ACS to authenticate as a site-admin user
Insight — On any SAML SP, test XML signature wrapping: relocate/duplicate the signed element and inject an unsigned attacker assertion the SP actually reads. Publicly available signed metadata gives you a valid signature to reuse. Tools: SAML Raider.
Program superhumanSurface webChain entityId collision -> victim SSO DoS -> user provisionTag samlTag account-takeover
Root cause
The SAML Response issuer is validated against trim(entityId), but the user is provisioned to the organization whose entityId matches with priority. Creating an org with the victim's entityId plus a trailing space makes legit users authenticate against the real IdP yet get placed into the attacker's org.
Method
Attacker creates a business account with entityId = victimEntityId + ' ' (trailing space) and own keypair
Legit users signing into the victim IdP now error out (DoS) because provisioning prefers the space-suffixed entity
When a user is (re)provisioned they land in the attacker's org
Attacker later renames their entityId and logs into the migrated user's account
Legit entityId: urn:victim:idp
Attacker entityId: urn:victim:idp (with trailing space)
# SAML validated vs trim(issuer); provisioning keyed to space-suffixed entity
Insight — Identifier normalization mismatches (trim/case/unicode) between the VALIDATION step and the LOOKUP/PROVISION step are a rich SAML/SSO bug class. Test near-duplicate entityIds differing only by whitespace, case, or trailing dot.
Program rocket_chatSurface webTag samlTag account-takeover
Root cause
Signature is validated against the first Signature element found anywhere in the XML, but assertions/attributes are then read from the first Response element found anywhere; the code never checks that the validated signature actually covers the Response it consumes. Attacker prepends an unsigned malicious Response.
Method
Configure SP for SAML login and capture the SAMLResponse POST
Take a validly-signed SAMLResponse from any legitimate login
Prepend a new, unsigned <Response> (attacker-chosen NameID/Email/OrganizationName) at the start, keeping the original signed element later
SP validates the original signature, then reads the attacker's first Response -> logs in as chosen (e.g. admin) user
# saml_utils.js sinks:
# signature: xpath(doc, "//*[local-name()='Signature' and namespace-uri()='http://www.w3.org/2000/09/xmldsig#']")[0]
# response: doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol','Response')[0]
# Attacker XML (schematic):
<samlp:Response> <!-- unsigned, attacker-controlled, parsed as [0] -->
<Assertion><Subject><NameID>admin</NameID></Subject>...</Assertion>
</samlp:Response>
<samlp:Response> <!-- original, validly signed -->
...<ds:Signature>...</ds:Signature>...
</samlp:Response>
Insight — SAML/XML-DSig is safe only if the validated signature reference is the SAME node you extract assertions from. Test every SAML SP by cloning a valid response and inserting an unsigned Response/Assertion before/inside the signed one (XSW variants). Also check status is validated after, not before, signature.
The SP validated the SAML response but did not track consumed assertion IDs, so a captured, still-valid SAMLResponse could be POSTed repeatedly, each time minting a fresh authenticated session.
Method
Complete SSO and capture the POST to /users/saml/auth containing the SAMLResponse
Replay the same POST in Repeater multiple times before it expires
Each replay returns a new valid session id - assertion is not consumed on first use
POST /users/saml/auth HTTP/1.1
Content-Type: application/x-www-form-urlencoded
SAMLResponse=<captured_base64_response>&RelayState=...
Insight — Test SAML/OIDC assertions for single-use: replay the captured response after login. An SP must store consumed assertion IDs until their NotOnOrAfter and reject repeats. A stolen assertion (MITM/XSS/phishing) becomes a reusable session key otherwise.