At a health research data-access portal, a posted SAMLResponse verifies cleanly yet admits a cohort nobody approved — how does XML signature wrapping achieve that?
answer
- no key is needed for this one
- the signature stays genuine and re-verifies
- move the original, forge the position
- verified by ID, consumed by position
- verify and read the same element
basics
~20 sThe genuine signed assertion is moved elsewhere in the XML tree and a forged copy is put in its place. Verification resolves the reference by ID and re-checks the moved original, while the application reads the element sitting in the expected position.
solid answer
~50 sThe attacker starts from a valid document — one the identity provider really signed, typically their own legitimate login. They relocate that signed `<saml:Assertion>` into a position the application never reads: `<samlp:Extensions>`, a `<saml:Advice>`, an added wrapper. Then they insert a forged assertion, carrying the attributes they want, where the original stood. The `<ds:Signature>` still names `URI="#_orig"`; the verifier looks that `ID` up anywhere in the document, finds the relocated genuine element, digests it, and gets a match. Verification reports success **for an element the application will never look at**. The application then does its own walk — the first `<saml:Assertion>` child of the `<samlp:Response>` — and reads the forgery. Nothing is broken cryptographically: the signature is real, the key is untouched. Two different selections were made over one document, and only one of them was checked.
code
xml · 32 lines<samlp:Response ID="_r1" Version="2.0" IssueInstant="2026-09-19T10:14:02Z">
<saml:Issuer>https://idp.research-federation.example/idp</saml:Issuer>
<samlp:Extensions>
<saml:Assertion ID="_orig" Version="2.0" IssueInstant="2026-09-19T10:11:47Z">
<saml:Issuer>https://idp.research-federation.example/idp</saml:Issuer>
<ds:Signature>
<ds:SignedInfo>
<ds:Reference URI="#_orig">
<ds:DigestValue>Kx9rQ2h1bmtvZmRpZ2VzdA==</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>Qm9ndXNzaWduYXR1cmV2YWx1ZQ==</ds:SignatureValue>
</ds:Signature>
<saml:AttributeStatement>
<saml:Attribute Name="urn:example:cohortScope">
<saml:AttributeValue>cohort-17</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Extensions>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion ID="_forged" Version="2.0" IssueInstant="2026-09-19T10:14:02Z">
<saml:Issuer>https://idp.research-federation.example/idp</saml:Issuer>
<saml:AttributeStatement>
<saml:Attribute Name="urn:example:cohortScope">
<saml:AttributeValue>all-cohorts</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>go deeper
Recall the one-line shape: the real signed element is moved aside and a fake one takes its place, so the check and the read look at different elements.
Explain the two selections — a reference resolved by ID during verification, a position walked during consumption — and why both succeed on the same document.
Diagnose it from a clean audit trail: a verified login granting entitlements the issuer never asserted, with no key compromise anywhere in the picture.
Treat it as an assurance question across partners you do not run: what evidence you would demand that a counterparty's receiver consumes only what it verified.
## The attack needs no key Signature wrapping is unusual among identity attacks because the attacker never touches cryptography. They need one thing: a document the identity provider genuinely signed, addressed to this service provider. Their own legitimate login supplies it. At a research data-access portal, that is an assertion saying they are a junior researcher with one approved cohort; what they want is an assertion saying they may open everything. They cannot edit the assertion — any edit changes the digest. So they do not edit it. They **move** it. ## The mechanism, step by step 1. Capture the valid `<samlp:Response>` containing `<saml:Assertion ID="_orig">` and its enveloped `<ds:Signature>`, whose single `<ds:Reference URI="#_orig">` digests it. 2. Relocate that whole signed assertion to a position the receiving application does not consume — inside `<samlp:Extensions>`, inside a `<saml:Advice>`, or inside a newly introduced wrapper element the receiver tolerates. 3. Build a forged `<saml:Assertion>`, typically a copy with a different `ID` and the attributes the attacker wants, and place it where the original was: the first assertion child of the `<samlp:Response>`. 4. Post the result. Now trace the receiver. The signature-verification step locates a `<ds:Signature>`, reads `URI="#_orig"`, and resolves that fragment **by searching the document for an element whose `ID` is `_orig`**. It finds the relocated original — unmodified, byte for byte after canonicalisation — computes the digest, matches it, checks `<ds:SignatureValue>` over `<ds:SignedInfo>` with the partner's key, and returns success. The application-logic step then asks the document a different question: *give me the assertion*. It answers with the element in the customary position, which is the forgery. | Step | Selects the element by | Which element it gets | |---|---|---| | Signature verification | `ID` lookup driven by the reference URI | The relocated, genuinely signed assertion | | Attribute consumption | Position in the tree | The forged assertion | **The defect is the gap between those two rows.** "The signature is valid" and "this data is signed" are different propositions, and the attack lives entirely in the space between them. ## Why the obvious objections do not stop it - **"The forged assertion is unsigned, so it will be rejected."** Only if something checks that the element consumed was the element verified. A receiver that asks "did the document verify?" and then parses the document afresh never makes that check. - **"Schema validation will reject the extra element."** Several of the classic placements are schema-valid — `<samlp:Extensions>` and `<saml:Advice>` exist precisely to carry additional content — and some receivers validate loosely once a signature has passed. - **"Encrypting the assertion prevents it."** Encryption changes who can read the document, not which element the receiver acts on after decrypting. - **"They would need the signing key."** They need no key. That is the point, and a candidate who answers "the identity provider's key must have leaked" has misread the whole class. - **"Duplicate IDs are illegal in XML."** They are — but `ID`-ness depends on the processor knowing the schema, and a document can be crafted so that the resolution a verifier performs and the traversal an application performs disagree about which element carries the identifier. ## The invariant Stated positively, the property a receiver needs is one sentence: **the element whose attributes are consumed must be the same element whose digest was verified** — not an element with the same name, not an element re-found by a second search over the document, the same one. Every failure in this class violates that sentence, and every interview question about wrapping is really asking whether the candidate can state it. How a particular service-provider implementation enforces it — which library, how the verified node is pinned, how assertion identifiers are remembered against replay — is an implementation subject and a different topic's ground. What belongs to the protocol is the reason the invariant is needed at all: XML Signature protects a subtree selected by reference, and an XML document is a tree in which subtrees can be moved without altering their bytes. ## What it costs when it lands At a data-access portal the assertion carries the entitlement, not just the identity — cohort scopes, consent classes, project membership. A wrapped response does not merely log someone in as somebody else; it hands them an authorisation decision the identity provider never made, and every downstream audit record shows a cleanly verified federated login. That is why this leaf is where SAML interviews get serious.
- Why does re-running verification a second time not reveal the substitution?Because it answers the same question again. The reference still resolves to the relocated genuine assertion, whose digest still matches. Verification is a statement about one subtree; repeating it produces the same true statement about the same subtree, and never touches the element the application read.
- Does signing the enclosing <samlp:Response> instead of the assertion close this?It raises the bar without changing the class. A signature over the `<samlp:Response>` covers its descendants, so a forgery inserted inside the digested subtree breaks the match. But the same relocate-and-substitute shape reappears wherever a receiver resolves the reference by search and then re-selects what it consumes, so the invariant — consume exactly what was verified — still does the work.
- Where does an attacker get validly signed material in the first place?Ordinarily from their own legitimate login: any account at the identity provider yields a validly signed assertion addressed to that service provider. No interception and no key compromise is required, which is why wrapping is an authorisation-escalation attack available to an insider holding the lowest possible entitlement.
A notarised page is quietly stapled at the back of the folder while a different page is slipped into the slot the clerk always opens. The notary's seal is genuine, the clerk's check passes, and the clerk never reads the page the seal covers.
saying these in an interview costs you the question
- Concludes the identity provider's signing key must have leaked.
- Says a valid signature means the whole document is signed.
- Believes schema validation on its own defeats the attack.
- Claims encrypting the assertion prevents element substitution.
- Thinks verifying the signature twice would detect it.
- Assumes the forged assertion must also carry a signature.