davsclaus commented on code in PR #26726:
URL: https://github.com/apache/camel/pull/26726#discussion_r4071655376


##########
components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/DefaultXmlSignature2Message.java:
##########
@@ -314,6 +350,96 @@ protected Node getNodeForMessageBodyInEnvelopingCase(Input 
input) throws Excepti
         return node;
     }
 
+    /**
+     * Checks that a validated Reference actually covered the document element 
the default search is about to emit.
+     * <p>
+     * Core signature validation only proves that each Reference's digest 
matches the content that Reference resolves
+     * to. It says nothing about the rest of the document. So an attacker can 
take a legitimately signed fragment, embed
+     * it unchanged inside a larger document of their own, and validation 
still passes - the same-document URI resolves
+     * to that fragment exactly as before - while this method would hand the 
whole attacker document downstream as
+     * verified content. That is XML signature wrapping.
+     * <p>
+     * The check is deliberately narrow, so that it rejects that shape and 
nothing else. It only complains when the
+     * signature carries same-document references and none of them covers the 
document element. A Reference with an
+     * empty URI covers the whole document, and a signature whose References 
are all external says nothing about this
+     * document either way, so both are left alone.
+     *
+     * @param input           the verification input, carrying the validated 
References
+     * @param documentElement the element the default search would emit
+     */
+    protected void checkDocumentElementIsCoveredByAReference(Input input, 
Element documentElement) throws Exception {
+        List<Reference> references = getReferencesForMessageMapping(input);
+        if (references == null || references.isEmpty()) {
+            return;
+        }
+
+        boolean sameDocumentReferenceSeen = false;
+        for (Reference reference : references) {
+            String uri = reference.getURI();
+            if (uri == null) {
+                // Nothing to correlate against
+                return;
+            }
+            if (uri.isEmpty()) {
+                // The whole document is covered
+                return;
+            }
+            if (!uri.startsWith("#")) {
+                // External reference - it tells us nothing about the document 
we are emitting
+                continue;
+            }
+            sameDocumentReferenceSeen = true;
+            String identifier = uri.substring(1);
+            if (identifier.startsWith("xpointer(/)")) {
+                // #xpointer(/) is the whole document
+                return;
+            }
+            if (coversElement(identifier, documentElement)) {
+                return;
+            }
+        }
+
+        if (sameDocumentReferenceSeen) {
+            throw new XmlSignatureException(
+                    "Cannot extract the root node for the output document from 
the XML signature document. "
+                                            + "None of the validated 
References covers the document element, so the "
+                                            + "document contains content which 
was not signed. Configure an output node "
+                                            + "search, or an 
XmlSignatureChecker, which selects the signed content.");
+        }
+    }
+
+    private static boolean coversElement(String identifier, Element 
documentElement) {
+        String xpointerId = getXPointerId(identifier);
+        String id = xpointerId != null ? xpointerId : identifier;
+
+        for (String attribute : ID_ATTRIBUTE_NAMES) {
+            if (id.equals(documentElement.getAttribute(attribute))) {

Review Comment:
   🔴 **An empty identifier matches any element.**
   
   `Element.getAttribute` returns `""` for a missing attribute, so when `id` is 
empty this is `"".equals("")` on the very first iteration and the document is 
accepted regardless of what it contains. Two reachable inputs produce an empty 
`id`:
   
   - `URI="#"` → `identifier = uri.substring(1)` = `""`, and 
`getXPointerId("")` returns null, so `id = ""`.
   - `URI="#xpointer(id(''))"` → `getXPointerId` strips the quotes and returns 
`""`.
   
   Both confirmed on this branch:
   
   ```
   PROBE-A: URI="#"               ACCEPTED (check bypassed)
   PROBE-B: #xpointer(id(''))     ACCEPTED (check bypassed)
   ```
   
   An empty identifier cannot name the document element, so it should simply 
not match:
   
   ```suggestion
           if (id.isEmpty()) {
               // an empty identifier names nothing - it must not fall through 
to the getAttribute comparison
               // below, where Element.getAttribute returns "" for a missing 
attribute and would match anything
               return false;
           }
           for (String attribute : ID_ATTRIBUTE_NAMES) {
               if (id.equals(documentElement.getAttribute(attribute))) {
   ```



##########
components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/DefaultXmlSignature2Message.java:
##########
@@ -314,6 +350,96 @@ protected Node getNodeForMessageBodyInEnvelopingCase(Input 
input) throws Excepti
         return node;
     }
 
+    /**
+     * Checks that a validated Reference actually covered the document element 
the default search is about to emit.
+     * <p>
+     * Core signature validation only proves that each Reference's digest 
matches the content that Reference resolves
+     * to. It says nothing about the rest of the document. So an attacker can 
take a legitimately signed fragment, embed
+     * it unchanged inside a larger document of their own, and validation 
still passes - the same-document URI resolves
+     * to that fragment exactly as before - while this method would hand the 
whole attacker document downstream as
+     * verified content. That is XML signature wrapping.
+     * <p>
+     * The check is deliberately narrow, so that it rejects that shape and 
nothing else. It only complains when the
+     * signature carries same-document references and none of them covers the 
document element. A Reference with an
+     * empty URI covers the whole document, and a signature whose References 
are all external says nothing about this
+     * document either way, so both are left alone.
+     *
+     * @param input           the verification input, carrying the validated 
References
+     * @param documentElement the element the default search would emit
+     */
+    protected void checkDocumentElementIsCoveredByAReference(Input input, 
Element documentElement) throws Exception {
+        List<Reference> references = getReferencesForMessageMapping(input);
+        if (references == null || references.isEmpty()) {
+            return;
+        }
+
+        boolean sameDocumentReferenceSeen = false;
+        for (Reference reference : references) {
+            String uri = reference.getURI();
+            if (uri == null) {

Review Comment:
   🔴 **This `return` exits the whole method, not just this iteration — so one 
null-URI Reference disables the check for every Reference after it.**
   
   Probed on this branch with references `[null, "#myID"]` against the wrapped 
document:
   
   ```
   PROBE-C: null-URI ref first -> ACCEPTED (check bypassed)
   ```
   
   The `#myID` reference never gets examined and `sameDocumentReferenceSeen` is 
never set, so the throw below is unreachable. The external-reference branch a 
few lines down already handles the "tells us nothing" case correctly with 
`continue` — this should match it. With `continue`, a lone null-URI reference 
still ends up accepted (nothing sets `sameDocumentReferenceSeen`), so the 
intended behaviour is preserved while the bypass closes.
   
   ```suggestion
               if (uri == null) {
                   // Nothing to correlate against - but keep looking at the 
remaining references
                   continue;
               }
   ```



##########
components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/DefaultXmlSignature2Message.java:
##########
@@ -142,8 +142,40 @@ public class DefaultXmlSignature2Message implements 
XmlSignature2Message {
      */
     public static final String OUTPUT_NODE_SEARCH_TYPE_XPATH = "XPath";
 
+    private static final String[] ID_ATTRIBUTE_NAMES = { "Id", "ID", "id" };

Review Comment:
   🟠 **Namespaced id attributes are not found, so legitimate documents are 
rejected.**
   
   These names are matched with the namespace-unaware `Element.getAttribute`, 
which compares qualified names. A document element carrying `wsu:Id="myID"` — 
the WS-Security convention, and a very plausible user of this option — is 
therefore rejected even though the Reference does cover it:
   
   ```
   PROBE-D: <signed xmlns:wsu="http://ns"; wsu:Id="myID"/> with URI="#myID"
            -> REJECTED: None of the validated References covers the document 
element
   ```
   
   The `getElementById` fallback below doesn't rescue it: that only works when 
a DTD or schema declared the attribute as type ID, which a plainly-parsed 
instance document has not.
   
   Either scan the element's attributes for a local name in this set regardless 
of namespace, or — if that is deliberately out of scope — say so in the 
`setEnforceReferenceCoverage` javadoc and the component docs, so operators know 
the option is for unprefixed `Id`/`ID`/`id` only.



##########
components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/DefaultXmlSignature2Message.java:
##########
@@ -155,7 +187,11 @@ public void mapToMessage(Input input, Message output) 
throws Exception {
                 node = getNodeForMessageBodyInEnvelopingCase(input);
             } else {
                 // enveloped or detached XML signature  --> remove signature 
element
-                node = input.getMessageBodyDocument().getDocumentElement();
+                Element documentElement = 
input.getMessageBodyDocument().getDocumentElement();
+                if (enforceReferenceCoverage) {
+                    checkDocumentElementIsCoveredByAReference(input, 
documentElement);

Review Comment:
   🟠 This branch — the actual wiring of the feature — is not covered by any 
test. `DefaultXmlSignature2MessageReferenceCoverageTest` calls 
`checkDocumentElementIsCoveredByAReference` directly with a stub `Input` and a 
stub `Reference`, so a regression that stopped calling it from here, or flipped 
the flag check, would not be caught.
   
   An end-to-end test through `xmlsecurity-verify` with a real signature over a 
sub-element, wrapped in an attacker document, would both cover this line and 
demonstrate the feature actually stops the attack it describes.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to