SAML XML Signature Wrapping moves a valid signature to cover benign content while the SP reads claims from a different node - confirming it uses a nonce NameID replay that establishes a session without impersonating any real user.
SAML XML Signature Wrapping is unusual among authentication bugs because the signature it exploits is entirely valid. The IdP signs a legitimate assertion; the attacker rearranges the XML document so that the signature still verifies against the benign element it always covered, while the Service Provider reads claims from a different element the attacker controls. The cryptography is correct; the application is just reading the wrong node.
An XML digital signature binds to the element identified by its URI attribute - the <ds:Reference URI="#assertion-id"> in the SignedInfo block. When the SP resolves that reference, it must find the element with the matching ID attribute and verify the signature against that specific serialization. Signature Wrapping exploits gaps in how parsers resolve that reference: if the SP finds the ID by simple document-order search rather than the canonicalized reference algorithm, an attacker can introduce a second element with the same ID, place it where the parser will find it first, and put the real (signed) assertion somewhere the parser ignores for claim extraction.
Probing XSW in a DAST context requires a legitimately obtained SAML response - one issued by the real IdP during an authenticated test flow. The manipulation is applied to that captured response before replay. A benign oracle keeps the probe safe: rather than forging a privileged role, the test inserts a controlled NameID value (e.g., a nonce like xsw-probe-62615533) that does not correspond to any real user. If the SP establishes a session and reflects that nonce in any response field - a greeting, a profile endpoint, or an error referencing the unknown user - the SP accepted the forged claim, confirming the wrapping attack. No real privilege is taken; no real user is impersonated.
import base64, copy from lxml import etree # Load the legitimately captured SAML response tree = etree.fromstring(base64.b64decode(captured_saml_b64)) ns = {"saml": "urn:oasis:names:tc:SAML:2.0:assertion"} # Clone the signed assertion and strip its signature element orig = tree.find(".//saml:Assertion", ns) forged = copy.deepcopy(orig) sig = forged.find(".//ds:Signature", {"ds":"http://www.w3.org/2000/09/xmldsig#"}) if sig is not None: forged.remove(sig) # Replace NameID with benign probe nonce nid = forged.find(".//saml:NameID", ns) nid.text = "xsw-probe-62615533" # Insert forged assertion before the original (XSW-1 variant) tree.insert(list(tree).index(orig), forged) # Re-encode and submit to the SP ACS endpoint payload = base64.b64encode(etree.tostring(tree)).decode() # POST payload to SP assertion-consumer URL, check for nonce in session
Eight documented XSW variants change where the original and forged elements are placed relative to the signature block and the response root. The confirming probe must cycle through applicable variants because a parser that correctly resolves XSW-1 may still be vulnerable to XSW-5 or XSW-8, which use different nesting strategies. OIDC introduces a parallel class of misconfigurations that operate on JWT claims rather than XML structure.
| Mechanism | Attacker-controlled input | Confirming artifact | Typical severity |
|---|---|---|---|
| XSW-1 to XSW-3 | Forged assertion before signed original | Session on nonce NameID; baseline replay returns error | Critical |
| XSW-4 to XSW-8 | Signed element inside wrapper siblings or extensions | Same differential; different document position | Critical |
| OIDC alg:none | id_token with no signature, alg header removed | Session granted on unsigned token; covered by JWT post | Critical |
| OIDC audience bypass | id_token issued for client-A replayed against client-B | Client-B accepts token despite mismatched aud claim | Critical if cross-tenant |
| OIDC PKCE downgrade | Authorization code intercepted without code_challenge | Token exchange succeeds without verifier | High - requires code interception |
OIDC audience bypass is particularly relevant in multi-tenant SaaS: if the SP validates the sub and iss claims but not aud, an id_token legitimately issued for one tenant's application can authenticate against another. The probe replays a captured token with an unmodified payload against a different SP endpoint; a granted session is the confirming artifact. Nothing is forged - the token is valid - only the target endpoint is wrong.
Authentication bypass is always critical, but the root cause differs between SAML and OIDC paths, which means the fix also differs. For SAML, the SP library must resolve the signed reference using strict canonicalization - the element found by GetElementById must be the same element verified by the signature, not just the first document-order match on the ID attribute. Libraries that perform this check correctly include OpenSAML 3+ and python3-saml >= 1.9.0; older versions and many hand-rolled parsers do not.
// Vulnerable: finds first element by attribute value (document order) Element assertion = (Element) doc.getElementsByTagNameNS(SAML_NS, "Assertion").item(0); // Safe: resolves the ID registered by the signature, not document order String refId = signedInfo.getReference(0).getURI().substring(1); Element assertion = doc.getElementById(refId); // must use setIdAttribute first // Ensure the element found here is the one verified above - no siblings
For OIDC audience validation, the SP must verify that the aud claim in the id_token matches the SP's own client ID, and must reject tokens where aud is a list that includes the SP ID alongside other audiences unless the azp (authorized party) claim is also validated. RFC 6749 and the OIDC Core specification both require this; it is a library default in most modern implementations but is silently disabled in several older SDKs when skip_audience_validation flags are set for development convenience and never reset.
Authentication bypass findings carry the full PoC: the modified SAML document or replayed OIDC token, the session cookie returned, and the /me endpoint response that confirms the forged identity was accepted. The severity is critical because the confirming artifact is a working session - reachability alone (an ACS endpoint that accepts POSTs) is not enough to raise this finding at that level.