This is an automated email from the ASF dual-hosted git repository.

robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-rampart.git

commit 9d0abc1ba487aabd5978ff32f909e1f9ee24434e
Author: Robert Lazarski <[email protected]>
AuthorDate: Tue Jun 9 16:04:30 2026 -1000

    RAMPART-428: validate the signed SOAP Body by identity, not by name
    
    The signed-parts check confirmed only that *some* element named "Body" was
    covered by the signature (actuallySigned.contains({soap}Body)). Because that
    test is name-based, a message that relocates the signed Body into a wrapper 
and
    places a different, unsigned Body in the Body position would still pass
    validation while the application consumed the unsigned Body (signature
    wrapping).
    
    validateSignedPartsHeaders now resolves the actual SOAP Body element that 
will
    be consumed (WSSecurityUtil.findBodyElement) and requires that exact 
element, by
    node identity, to be among the elements the signature covered. The signed
    elements are now tracked as DOM elements alongside their QNames for this 
check.
    
    Adds PolicyBasedResultsValidatorTest covering the identity-based decision
    (relocated same-name body rejected, actual signed body accepted). Verified 
with a
    full clean -Papache-release verify across all modules, including the 
signed-body
    integration scenarios and the nine policy samples, on JDK 25.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../rampart/PolicyBasedResultsValidator.java       | 46 ++++++++--
 .../rampart/PolicyBasedResultsValidatorTest.java   | 99 ++++++++++++++++++++++
 2 files changed, 136 insertions(+), 9 deletions(-)

diff --git 
a/modules/rampart-core/src/main/java/org/apache/rampart/PolicyBasedResultsValidator.java
 
b/modules/rampart-core/src/main/java/org/apache/rampart/PolicyBasedResultsValidator.java
index a06a0ac8..250d7a52 100644
--- 
a/modules/rampart-core/src/main/java/org/apache/rampart/PolicyBasedResultsValidator.java
+++ 
b/modules/rampart-core/src/main/java/org/apache/rampart/PolicyBasedResultsValidator.java
@@ -590,6 +590,25 @@ public class PolicyBasedResultsValidator implements 
ExtendedPolicyValidatorCallb
        
     }
 
+    /**
+     * Returns true only if {@code target} is, by node identity, one of the 
elements
+     * actually covered by the signature. This is the core of the 
signature-wrapping
+     * defence (RAMPART-428): an attacker can place an element with the same 
QName as
+     * a signed element elsewhere in the message, so a name-based check is not 
enough -
+     * the consumed element itself must be the signed one.
+     */
+    static boolean isElementSigned(Element target, List<Element> 
signedElements) {
+        if (target == null || signedElements == null) {
+            return false;
+        }
+        for (Element signedElement : signedElements) {
+            if (signedElement != null && signedElement.isSameNode(target)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     protected void validateSignedPartsHeaders(ValidatorData data, 
List<WSEncryptionPart> signatureParts,
                                               List<WSSecurityEngineResult> 
results)
     throws RampartException {
@@ -599,8 +618,12 @@ public class PolicyBasedResultsValidator implements 
ExtendedPolicyValidatorCallb
        
         WSSecurityEngineResult[] actionResults = fetchActionResults(results, 
WSConstants.SIGN);
 
-        // Find elements that are signed
+        // Find elements that are signed. We keep both the QNames (used for 
header
+        // checks) and the actual signed DOM elements. The latter lets us 
verify the
+        // signed element by identity rather than just by name, which is 
required to
+        // defeat signature-wrapping attacks (RAMPART-428).
         List<QName> actuallySigned = new ArrayList<QName>();
+        List<Element> actuallySignedElements = new ArrayList<Element>();
         if (actionResults != null) {
             for (WSSecurityEngineResult actionResult : actionResults) {
 
@@ -620,6 +643,7 @@ public class PolicyBasedResultsValidator implements 
ExtendedPolicyValidatorCallb
                                 String ns = 
(nodeList.item(x)).getNamespaceURI();
                                 String ln = (nodeList.item(x)).getLocalName();
                                 actuallySigned.add(new QName(ns, ln));
+                                actuallySignedElements.add((Element) 
nodeList.item(x));
                                 break;
                             }
                         }
@@ -627,6 +651,7 @@ public class PolicyBasedResultsValidator implements 
ExtendedPolicyValidatorCallb
                         String ns = protectedElement.getNamespaceURI();
                         String ln = protectedElement.getLocalName();
                         actuallySigned.add(new QName(ns, ln));
+                        actuallySignedElements.add(protectedElement);
                     }
                 }
 
@@ -636,16 +661,19 @@ public class PolicyBasedResultsValidator implements 
ExtendedPolicyValidatorCallb
         for (WSEncryptionPart wsep : signatureParts) {
             if (wsep.getName().equals(WSConstants.ELEM_BODY)) {
 
-                QName bodyQName;
+                // RAMPART-428: verify the actual SOAP Body element that the
+                // application will consume is the very element that was 
signed,
+                // not merely that *some* element named "Body" was signed. A
+                // signature-wrapping attack relocates the signed body into a
+                // wrapper and inserts a new, unsigned body in the Body 
position;
+                // a QName-only check would pass that message. Comparing by 
node
+                // identity rejects it.
+                Element actualBody = 
WSSecurityUtil.findBodyElement(rmd.getDocument());
 
-                if 
(WSConstants.URI_SOAP11_ENV.equals(envelope.getNamespaceURI())) {
-                    bodyQName = new SOAP11Constants().getBodyQName();
-                } else {
-                    bodyQName = new SOAP12Constants().getBodyQName();
-                }
+                boolean bodySigned = isElementSigned(actualBody, 
actuallySignedElements);
 
-                if (!actuallySigned.contains(bodyQName) && 
!rmd.getPolicyData().isSignBodyOptional()) {
-                    // soap body is not signed
+                if (!bodySigned && !rmd.getPolicyData().isSignBodyOptional()) {
+                    // soap body is not signed (or a different element was 
signed)
                     throw new RampartException("bodyNotSigned");
                 }
 
diff --git 
a/modules/rampart-core/src/test/java/org/apache/rampart/PolicyBasedResultsValidatorTest.java
 
b/modules/rampart-core/src/test/java/org/apache/rampart/PolicyBasedResultsValidatorTest.java
new file mode 100644
index 00000000..47e3a26f
--- /dev/null
+++ 
b/modules/rampart-core/src/test/java/org/apache/rampart/PolicyBasedResultsValidatorTest.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.rampart;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import junit.framework.TestCase;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+/**
+ * Tests the signature-wrapping defence in {@link PolicyBasedResultsValidator}
+ * (RAMPART-428): the SOAP Body the application consumes must be the very 
element
+ * that was signed, identified by node identity rather than by element name.
+ */
+public class PolicyBasedResultsValidatorTest extends TestCase {
+
+    private static final String SOAP_NS = 
"http://schemas.xmlsoap.org/soap/envelope/";;
+
+    // A signature-wrapping layout: the original (signed) Body is relocated 
into a
+    // <wrapper> inside the header, and a new arbitrary Body sits in the Body 
position.
+    private static final String WRAPPED =
+            "<env:Envelope xmlns:env='" + SOAP_NS + "'>"
+          + "  <env:Header>"
+          + "    <wrapper xmlns=''>"
+          + "      <env:Body env:id='signed'><echo>original</echo></env:Body>"
+          + "    </wrapper>"
+          + "  </env:Header>"
+          + "  <env:Body><echo>arbitrary</echo></env:Body>"
+          + "</env:Envelope>";
+
+    private Document doc;
+    private Element realBody;     // the Body the application consumes (Body 
position)
+    private Element wrappedBody;  // the relocated, signed Body
+
+    protected void setUp() throws Exception {
+        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+        dbf.setNamespaceAware(true);
+        DocumentBuilder db = dbf.newDocumentBuilder();
+        doc = db.parse(new 
ByteArrayInputStream(WRAPPED.getBytes(StandardCharsets.UTF_8)));
+
+        NodeList bodies = doc.getElementsByTagNameNS(SOAP_NS, "Body");
+        assertEquals("test fixture should contain two Body elements", 2, 
bodies.getLength());
+        // Document order: the wrapped (signed) Body comes first (inside the 
header),
+        // the real consumed Body is the direct child of Envelope.
+        wrappedBody = (Element) bodies.item(0);
+        realBody = (Element) bodies.item(1);
+        assertEquals("Envelope", realBody.getParentNode().getLocalName());
+        assertEquals("wrapper", wrappedBody.getParentNode().getLocalName());
+    }
+
+    /** Wrapping attack: only the relocated copy is signed, so the real body 
must be rejected. */
+    public void testWrappedBodyIsNotConsideredSigned() {
+        assertFalse("a relocated same-name Body must not count as the signed 
body",
+                PolicyBasedResultsValidator.isElementSigned(realBody, 
Collections.singletonList(wrappedBody)));
+    }
+
+    /** Legitimate case: the real consumed body is itself the signed element. 
*/
+    public void testActualSignedBodyIsAccepted() {
+        assertTrue("the actual signed body must be recognised",
+                PolicyBasedResultsValidator.isElementSigned(realBody, 
Collections.singletonList(realBody)));
+    }
+
+    /** Mixed set still matches by identity. */
+    public void testAcceptedWhenRealBodyAmongSignedElements() {
+        assertTrue(PolicyBasedResultsValidator.isElementSigned(
+                realBody, Arrays.asList(wrappedBody, realBody)));
+    }
+
+    public void testNullInputsAreNotSigned() {
+        assertFalse(PolicyBasedResultsValidator.isElementSigned(null, 
Collections.singletonList(realBody)));
+        assertFalse(PolicyBasedResultsValidator.isElementSigned(realBody, 
null));
+        assertFalse(PolicyBasedResultsValidator.isElementSigned(realBody, 
Collections.<Element>emptyList()));
+    }
+}

Reply via email to