Skip to content

Spring Security between 6.3.10 (exclusive) and at least 6.5.11 (inclusive) breaks SAML signature validation due to namespace hoisting #19606

Description

@martingrau-wps

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

  1. xmlns:xs="http://www.w3.org/2001/XMLSchema" at each saml:AttributeValue
  2. Decryption process does not change namespace positioning at all
  3. 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

  1. xmlns:xs="http://www.w3.org/2001/XMLSchema" NOT at each saml:AttributeValue anymore but instead on the parent saml:Assertion
  2. 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
  3. 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
  4. The error persists in OpenSaml5Template of Spring Security (tested in 7.0.6 & 7.1.1)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions