This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch 2_4_x-fixes in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
commit b2a91608e127ac05d7736e27bb7676d2d8721ae0 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Mon Sep 21 15:42:33 2026 +0100 StAX SAML subject confirmation skipped for an empty SOAP Body (#724) --- .../processor/input/SAMLTokenInputHandler.java | 12 +++++ .../stax/test/saml/SAMLTokenNegativeTest.java | 55 ++++++++++++++++++++++ .../resources/testdata/empty-body-soap-1.1.xml | 4 ++ 3 files changed, 71 insertions(+) diff --git a/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SAMLTokenInputHandler.java b/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SAMLTokenInputHandler.java index e70918816..25f6dc75b 100644 --- a/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SAMLTokenInputHandler.java +++ b/ws-security-stax/src/main/java/org/apache/wss4j/stax/impl/processor/input/SAMLTokenInputHandler.java @@ -69,6 +69,7 @@ import org.apache.xml.security.stax.ext.InputProcessorChain; import org.apache.xml.security.stax.ext.XMLSecurityConstants; import org.apache.xml.security.stax.ext.XMLSecurityProperties; import org.apache.xml.security.stax.ext.stax.XMLSecAttribute; +import org.apache.xml.security.stax.ext.stax.XMLSecEndElement; import org.apache.xml.security.stax.ext.stax.XMLSecEvent; import org.apache.xml.security.stax.ext.stax.XMLSecNamespace; import org.apache.xml.security.stax.ext.stax.XMLSecStartElement; @@ -604,6 +605,17 @@ public class SAMLTokenInputHandler extends AbstractInputSecurityHeaderHandler { inputProcessorChain.removeProcessor(this); checkPossessionOfKey(inputProcessorChain, samlAssertionWrapper, subjectSecurityToken); } + } else if (xmlSecEvent.getEventType() == XMLStreamConstants.END_ELEMENT) { + // A Body with no element child of its own never produces the event above, so + // without this the subject confirmation of the assertion would go unchecked for + // such a message. The Body end element is the last point at which the check can + // still be made, and every Signature covering the Body has been seen by then. + XMLSecEndElement xmlSecEndElement = xmlSecEvent.asEndElement(); + List<QName> elementPath = xmlSecEndElement.getElementPath(); + if (elementPath.size() == 2 && WSSUtils.isInSOAPBody(elementPath)) { + inputProcessorChain.removeProcessor(this); + checkPossessionOfKey(inputProcessorChain, samlAssertionWrapper, subjectSecurityToken); + } } return xmlSecEvent; } diff --git a/ws-security-stax/src/test/java/org/apache/wss4j/stax/test/saml/SAMLTokenNegativeTest.java b/ws-security-stax/src/test/java/org/apache/wss4j/stax/test/saml/SAMLTokenNegativeTest.java index 4830a0945..6a2ae5682 100755 --- a/ws-security-stax/src/test/java/org/apache/wss4j/stax/test/saml/SAMLTokenNegativeTest.java +++ b/ws-security-stax/src/test/java/org/apache/wss4j/stax/test/saml/SAMLTokenNegativeTest.java @@ -233,6 +233,61 @@ public class SAMLTokenNegativeTest extends AbstractTestBase { } } + /** + * The proof-of-possession check for a holder-of-key assertion used to be triggered by the + * first element inside the SOAP Body. A Body with no element child of its own produces no + * such event, so the check never ran and an assertion whose subject key the sender does not + * hold was accepted. + */ + @Test + public void testHOKEmptyBodyInbound() throws Exception { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + { + SAML1CallbackHandler callbackHandler = new SAML1CallbackHandler(); + callbackHandler.setStatement(SAML1CallbackHandler.Statement.AUTHN); + callbackHandler.setConfirmationMethod(SAML1Constants.CONF_HOLDER_KEY); + callbackHandler.setIssuer("www.example.com"); + // The assertion itself is signed, so it gets past the subject confirmation method + // check; what it does not carry is any proof that the sender holds the subject key. + callbackHandler.setSignAssertion(true); + + InputStream sourceDocument = + this.getClass().getClassLoader().getResourceAsStream("testdata/empty-body-soap-1.1.xml"); + String action = WSHandlerConstants.SAML_TOKEN_UNSIGNED; + Properties properties = new Properties(); + properties.put(WSHandlerConstants.SAML_CALLBACK_REF, callbackHandler); + Document securedDocument = doOutboundSecurityWithWSS4J(sourceDocument, action, properties); + + // The Body must have stayed empty, or the test is not exercising what it claims to + NodeList bodies = + securedDocument.getElementsByTagNameNS( + "http://schemas.xmlsoap.org/soap/envelope/", "Body"); + assertEquals(1, bodies.getLength()); + assertEquals(0, ((Element)bodies.item(0)).getElementsByTagName("*").getLength()); + + javax.xml.transform.Transformer transformer = TRANSFORMER_FACTORY.newTransformer(); + transformer.transform(new DOMSource(securedDocument), new StreamResult(baos)); + } + + { + WSSSecurityProperties securityProperties = new WSSSecurityProperties(); + securityProperties.loadSignatureVerificationKeystore( + this.getClass().getClassLoader().getResource("saml/issuer.jks"), "default".toCharArray()); + InboundWSSec wsSecIn = WSSec.getInboundWSSec(securityProperties, false, true); + XMLStreamReader xmlStreamReader = + wsSecIn.processInMessage( + xmlInputFactory.createXMLStreamReader(new ByteArrayInputStream(baos.toByteArray()))); + + try { + StAX2DOM.readDoc(documentBuilderFactory.newDocumentBuilder(), xmlStreamReader); + fail("XMLStreamException expected"); + } catch (XMLStreamException e) { + assertNotNull(e.getCause()); + } + } + } + @Test public void testSAML2TrustFailureInbound() throws Exception { diff --git a/ws-security-stax/src/test/resources/testdata/empty-body-soap-1.1.xml b/ws-security-stax/src/test/resources/testdata/empty-body-soap-1.1.xml new file mode 100644 index 000000000..9b9596b28 --- /dev/null +++ b/ws-security-stax/src/test/resources/testdata/empty-body-soap-1.1.xml @@ -0,0 +1,4 @@ +<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"> + <env:Header/> + <env:Body/> +</env:Envelope>
