spring-projects / spring-projects/spring-security
Spring Security between 6.3.10 (exclusive) and at least 6.5.11 (inclusive) breaks SAML signature validation due to namespace hoisting
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 9.6k
- Forks
- 6.3k
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 52
Description
Description
Breaking change and possible bug introduced somewhere between Spring Security 6.3.10 (exclusive) and 6.5.11 (inclusive)
When Spring Security processes encrypted SAML assertions, it calls XMLObjectSupport.marshall(XMLObject xmlObject) to reestablish ID attributes.
This has the unintended (?) side effect of hoisting non-visible namespace declarations from inner elements to the root response element and in the following process to the assertion element, changing the XML canonical form.
Since XML signatures are calculated over the exact canonical form, this namespace reorganization causes digest recalculation to produce a different hash, resulting in signature validation failure (due to different assertion digest values).
Root Cause
XMLObjectSupport.marshall(response) in OpenSaml4Template.OpenSaml4DecryptionConfigurer.decryptResponse(XmlObject xmlObject)
Fix
- Replace XMLObjectSupport.marshall(XMLObject xmlObject) with targeted ID attribute reestablishment that preserves original namespace declarations.
Workaround
- We commented out the following lines of code on the method OpenSaml4Template.decryptResponse(Response respone) (as well as in newer versions: OpenSaml5Template)
// This original code is currently commented out, because it leads to namespace hoisting of the xs namespace which in turn broke assertion
// if (!decrypteds.isEmpty()) {
// try {
// XMLObjectSupport.marshall(response);
// } catch (MarshallingException ex) {
// throw new Saml2Exception(ex);
// }
// }
Affected Versions: Spring Security between 6.3.10 (exclusive) and at least 6.5.11 (inclusive)
Evidence
Important to look at:
Positioning of the declaration of the xs namespace: xmlns:xs="http://www.w3.org/2001/XMLSchema" in the two different versions
Spring Security 6.3.10 (working):
Response
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_14082bfd-559c-44ab-979a-037d6289b3fc" InResponseTo="ARQ8c84f77-1b4f-4049-acd8-e8f2e204c02e" Version="2.0" IssueInstant="2026-08-17T13:55:25.094Z" Destination="https://url/saml/SSO">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:issuer</saml:Issuer>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
...
</Signature>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
...
</saml:EncryptedAssertion>
</samlp:Response>
Assertion for digest value calculation
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_c1986f24-2407-4cbe-a34e-61c87e7f82ed" IssueInstant="2026-08-17T13:55:25.083Z" Version="2.0">
<saml:AttributeStatement>
<saml:Attribute Name="PrincipalType" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string"> Employee </saml:AttributeValue>
</saml:Attribute>
</saml:Assertion>
Findings
xmlns:xs="http://www.w3.org/2001/XMLSchema"at each saml:AttributeValue- Decryption process does not change namespace positioning at all
- Digest value verification works
Spring Security 6.5.11 (NOT working):
Response after decryption
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:q1="schemas:identity" xmlns:xs="http://www.w3.org/2001/XMLSchema" Destination="https://url.de:443/saml/SSO" ID="_64439ff0-81a5-4d2d-a941-89b74c48e0d3" InResponseTo="ARQ9bb34f1-300d-4551-bff3-d1668dafbedb" IssueInstant="2026-08-17T11:27:28.092Z" Version="2.0">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:issuer</saml:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
...
</ds:Signature>
<samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
...
</samlp:Status>
<saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
...
</saml:EncryptedAssertion>
...
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_fd3c7555-b04d-4f02-a8d9-8c6c267df67b" IssueInstant="2026-08-17T11:27:28.081Z" Version="2.0">
<saml:Issuer>urn:issuer</saml:Issuer>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
...
</Signature>
<saml:Subject>
...
</saml:Subject>
<saml:Conditions NotOnOrAfter="2026-08-17T21:27:28.073Z">
...
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2026-08-17T11:27:28.073Z" SessionIndex="a2275023-023b-45d2-ae23-cebb642d8ce4">
<saml:AuthnContext>
...
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="PrincipalType" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string"> Employee </saml:AttributeValue>
</saml:Attribute>
</saml:Assertion>
</samlp:Response>
Assertion for digest value calculation
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_047d1fd0-d743-4a31-81dd-10bcd69a7b08" IssueInstant="2026-08-18T06:10:37.756Z" Version="2.0">
<saml:Issuer>urn:issuer</saml:Issuer>
<saml:Subject>
...
</saml:Subject>
<saml:Conditions NotOnOrAfter="2026-08-18T16:10:37.746Z">
...
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2026-08-18T06:10:37.746Z" SessionIndex="e30d1947-8cc5-4d90-929b-71557273bdb1">
...
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="PrincipalType" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">Employee</saml:AttributeValue>
</saml:Attribute>
...
</saml:AttributeStatement>
</saml:Assertion>
Findings
xmlns:xs="http://www.w3.org/2001/XMLSchema"NOT at each saml:AttributeValue anymore but instead on the parent saml:Assertion- After decryption process XMLObjectSupport.marshall(response) moves [in OpenSaml4Template.OpenSaml4DecryptionConfigurer.decryptResponse(XmlObject xmlObject)]
- the non-visible namespace "xs" to response and then to the assertion-tag in preparation of the digest verification
- but NOT back to the saml:AttributeValue where it is supposed to be for the correct digest value calculation
- C14N works properly with the "corrupted" assertion
- the "xs" namespace is nonVisbible, because it is declared only on the value side of an xmlns (xsi:type="xs:string") and cannot be rearranged back to the children by the C14N transformation algorithm
- The error persists in OpenSaml5Template of Spring Security (tested in 7.0.6 & 7.1.1)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in OpenSaml4Template.OpenSaml4DecryptionConfigurer.decryptResponse(XmlObject xmlObject) and compare the XMLObjectSupport.marshall(response) path with the OpenSaml5Template behavior. Reproduce encrypted SAML assertion signature validation with the namespace examples, then verify that ID reestablishment preserves the original namespace positioning and the assertion digest validates.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring
- Domain
- authentication, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100