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

coheigea pushed a commit to branch coheigea/wsdl-import
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit c173d19db41ac2fd3f63f66bb9721debf3819def
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Jul 20 13:26:33 2026 +0100

    Read WSDL imports through CXF's StaxUtils instead of WSDL4J
---
 .../cxf/wsdl11/AbstractWrapperWSDLLocator.java     | 39 +++++++++++
 .../org/apache/cxf/wsdl11/WSDLManagerImpl.java     | 35 +++++++++-
 .../org/apache/cxf/wsdl11/WSDLManagerImplTest.java | 49 ++++++++++++++
 .../apache/cxf/wsdl11/wsdl_xxe_import_main.wsdl    | 76 ++++++++++++++++++++++
 .../cxf/wsdl11/wsdl_xxe_import_malicious.xsd       | 49 ++++++++++++++
 5 files changed, 246 insertions(+), 2 deletions(-)

diff --git 
a/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/AbstractWrapperWSDLLocator.java 
b/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/AbstractWrapperWSDLLocator.java
index 0b16b42f435..75ceb9507ba 100644
--- 
a/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/AbstractWrapperWSDLLocator.java
+++ 
b/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/AbstractWrapperWSDLLocator.java
@@ -18,15 +18,22 @@
  */
 package org.apache.cxf.wsdl11;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 
 import javax.wsdl.xml.WSDLLocator;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+import org.w3c.dom.Document;
 
 import org.xml.sax.InputSource;
 
 import org.apache.cxf.resource.URIResolver;
+import org.apache.cxf.staxutils.StaxUtils;
 
 public abstract class AbstractWrapperWSDLLocator implements WSDLLocator {
 
@@ -106,6 +113,38 @@ public abstract class AbstractWrapperWSDLLocator 
implements WSDLLocator {
                 lastImport = src.getSystemId();
             }
         }
+
+        // Pre-parse imported documents through CXF's hardened StaxUtils path 
to neutralize
+        // any XXE payloads (CWE-611) before handing the InputSource back to 
WSDL4J,
+        // whose DocumentBuilderFactory is not configured with XXE protections.
+        // The serialised output will contain no DOCTYPE declarations or 
unresolved entities.
+        if (src != null && (src.getByteStream() != null || 
src.getCharacterStream() != null)) {
+            String savedSystemId = src.getSystemId();
+            String savedPublicId = src.getPublicId();
+            XMLStreamReader xmlReader = null;
+            try {
+                xmlReader = StaxUtils.createXMLStreamReader(src);
+                Document doc = StaxUtils.read(xmlReader, true);
+                ByteArrayOutputStream bos = new ByteArrayOutputStream();
+                StaxUtils.writeTo(doc, bos);
+                InputSource hardenedSrc = new InputSource(new 
ByteArrayInputStream(bos.toByteArray()));
+                hardenedSrc.setSystemId(savedSystemId);
+                hardenedSrc.setPublicId(savedPublicId);
+                src = hardenedSrc;
+            } catch (XMLStreamException e) {
+                throw new RuntimeException("Failed to securely parse WSDL/XSD 
import '"
+                                           + importLocation + "': " + 
e.getMessage(), e);
+            } finally {
+                if (xmlReader != null) {
+                    try {
+                        StaxUtils.close(xmlReader);
+                    } catch (XMLStreamException ex) {
+                        // ignore close failure
+                    }
+                }
+            }
+        }
+
         return src;
     }
 
diff --git a/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/WSDLManagerImpl.java 
b/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/WSDLManagerImpl.java
index 460a647b852..d4e2fbeb309 100644
--- a/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/WSDLManagerImpl.java
+++ b/rt/wsdl/src/main/java/org/apache/cxf/wsdl11/WSDLManagerImpl.java
@@ -36,6 +36,7 @@ import javax.wsdl.extensions.ExtensibilityElement;
 import javax.wsdl.extensions.ExtensionRegistry;
 import javax.wsdl.extensions.mime.MIMEPart;
 import javax.wsdl.factory.WSDLFactory;
+import javax.wsdl.xml.WSDLLocator;
 import javax.wsdl.xml.WSDLReader;
 import javax.xml.namespace.QName;
 import javax.xml.stream.XMLStreamException;
@@ -179,15 +180,45 @@ public class WSDLManagerImpl implements WSDLManager {
         reader.setFeature("javax.wsdl.verbose", false);
         reader.setExtensionRegistry(registry);
 
+        // Use a WSDLLocator even when loading from a DOM element so that any
+        // <wsdl:import> or <xsd:import> inside the element goes through
+        // AbstractWrapperWSDLLocator.getImportInputSource(), which pre-parses
+        // each imported document through CXF's hardened StaxUtils path before
+        // handing it to WSDL4J's DocumentBuilderFactory (CWE-611).
+        String documentBaseUri = el.getOwnerDocument().getDocumentURI();
+        if (documentBaseUri == null) {
+            documentBaseUri = "";
+        }
+        final WSDLLocator wsdlLocator;
+        if (bus != null) {
+            CatalogWSDLLocator catLocator = new 
CatalogWSDLLocator(documentBaseUri, bus);
+            wsdlLocator = new ResourceManagerWSDLLocator(documentBaseUri, 
catLocator, bus);
+        } else {
+            // No bus: fall back to catalog-only resolution. The anonymous 
subclass
+            // satisfies AbstractWrapperWSDLLocator's abstract methods and 
still
+            // applies hardening to whatever the catalog can resolve.
+            final CatalogWSDLLocator catLocator = new 
CatalogWSDLLocator(documentBaseUri);
+            wsdlLocator = new AbstractWrapperWSDLLocator(documentBaseUri, 
catLocator) {
+                @Override
+                public InputSource getInputSource() {
+                    return catLocator.getBaseInputSource();
+                }
+                @Override
+                public InputSource getInputSource(String parentLocation, 
String importLocation) {
+                    return new InputSource(); // no ResourceManager without a 
bus
+                }
+            };
+        }
+
         final Definition def;
 
         // This is needed to avoid security exceptions when running with a 
security manager
         if (System.getSecurityManager() == null) {
-            def = reader.readWSDL("", el);
+            def = reader.readWSDL(wsdlLocator, el);
         } else {
             try {
                 def = AccessController.doPrivileged(
-                        (PrivilegedExceptionAction<Definition>) () -> 
reader.readWSDL("", el));
+                        (PrivilegedExceptionAction<Definition>) () -> 
reader.readWSDL(wsdlLocator, el));
             } catch (PrivilegedActionException paex) {
                 throw new WSDLException(WSDLException.PARSER_ERROR, 
paex.getMessage(), paex);
             }
diff --git 
a/rt/wsdl/src/test/java/org/apache/cxf/wsdl11/WSDLManagerImplTest.java 
b/rt/wsdl/src/test/java/org/apache/cxf/wsdl11/WSDLManagerImplTest.java
index 66bb3353319..51031005e52 100644
--- a/rt/wsdl/src/test/java/org/apache/cxf/wsdl11/WSDLManagerImplTest.java
+++ b/rt/wsdl/src/test/java/org/apache/cxf/wsdl11/WSDLManagerImplTest.java
@@ -43,6 +43,7 @@ import org.apache.cxf.wsdl.WSDLManager;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -226,4 +227,52 @@ public class WSDLManagerImplTest {
             // expected
         }
     }
+
+    /**
+     * Regression test for CWE-611: XML External Entity (XXE) injection via 
WSDL/XSD imports.
+     *
+     * When a WSDL contains a {@code <wsdl:import>} or {@code <xsd:import>} 
that resolves
+     * to a document carrying a DTD with an external SYSTEM entity, CXF must 
not allow that
+     * entity to be resolved or expanded. The top-level WSDL is already parsed 
through the
+     * hardened {@code StaxUtils} path; this test verifies the same protection 
is applied
+     * to all transitively imported documents by {@link 
AbstractWrapperWSDLLocator}.
+     *
+     * <p>Without the fix, WSDL4J's {@code DocumentBuilderFactory} (configured 
with only
+     * {@code setNamespaceAware(true)}) would attempt to resolve the external 
entity,
+     * enabling file disclosure, blind SSRF, or entity-expansion DoS.
+     * Related: CWE-611, CWE-776, CWE-918.
+     */
+    @Test
+    public void testWSDLImportXXEVulnerability() throws Exception {
+        String wsdlUrl = 
getClass().getResource("wsdl_xxe_import_main.wsdl").toString();
+
+        WSDLManager builder = new WSDLManagerImpl();
+
+        // With the fix in place AbstractWrapperWSDLLocator pre-parses 
imported documents
+        // through StaxUtils (DTD off, external entities off) before WSDL4J 
ever sees them.
+        // Either:
+        //   (a) loading succeeds and no file content is leaked into the 
definition, OR
+        //   (b) loading fails because StaxUtils itself rejects the DOCTYPE – 
either outcome
+        //       proves the malicious entity was not handed to WSDL4J's 
unhardenened parser.
+        try {
+            Definition def = builder.getDefinition(wsdlUrl);
+
+            // (a) Loaded successfully: assert the sentinel file content is 
not present.
+            assertNotNull("WSDL Definition should be parsed", def);
+            java.io.ByteArrayOutputStream bos = new 
java.io.ByteArrayOutputStream();
+            builder.getWSDLFactory().newWSDLWriter().writeWSDL(def, bos);
+            String serialized = 
bos.toString(java.nio.charset.StandardCharsets.UTF_8.name());
+            // The entity target (/etc/hostname content) must not appear in 
the output
+            assertFalse("XXE entity content must not be present in parsed WSDL 
definition",
+                        serialized.contains("xxe_test_file"));
+        } catch (Exception e) {
+            // (b) StaxUtils rejected the DTD before WSDL4J could act on it.
+            // Any error here must originate from CXF's own hardened path, not 
from
+            // WSDL4J's unhardenened DocumentBuilderFactory.
+            String message = e.getMessage() == null ? "" : e.getMessage();
+            assertFalse("WSDL4J's unhardened parser must not reach entity 
resolution; "
+                        + "only CXF's StaxUtils path should produce errors. 
Got: " + message,
+                        
message.contains("com.ibm.wsdl.xml.WSDLReaderImpl.getDocument"));
+        }
+    }
 }
\ No newline at end of file
diff --git 
a/rt/wsdl/src/test/resources/org/apache/cxf/wsdl11/wsdl_xxe_import_main.wsdl 
b/rt/wsdl/src/test/resources/org/apache/cxf/wsdl11/wsdl_xxe_import_main.wsdl
new file mode 100644
index 00000000000..3d613c93485
--- /dev/null
+++ b/rt/wsdl/src/test/resources/org/apache/cxf/wsdl11/wsdl_xxe_import_main.wsdl
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<!-- Test case for CWE-611: XXE via WSDL imports
+     This WSDL imports a malicious XSD that contains an XXE payload.
+     The top-level WSDL is benign, but the imported document carries the XXE.
+     This tests whether WSDL4J properly hardens the parser for imported 
documents.
+-->
+<wsdl:definitions 
+    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/";
+    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/";
+    xmlns:tns="http://apache.org/hello_world_xxe";
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema";
+    targetNamespace="http://apache.org/hello_world_xxe";
+    name="HelloWorldXXE">
+
+    <!-- Import a schema that contains XXE payload -->
+    <wsdl:types>
+        <xsd:schema targetNamespace="http://apache.org/hello_world_xxe";>
+            <xsd:import namespace="http://apache.org/xxe_schemas"; 
schemaLocation="wsdl_xxe_import_malicious.xsd"/>
+            <xsd:element name="requestElement" type="xsd:string"/>
+            <xsd:element name="responseElement" type="xsd:string"/>
+        </xsd:schema>
+    </wsdl:types>
+
+    <wsdl:message name="sayHiRequest">
+        <wsdl:part name="body" element="tns:requestElement"/>
+    </wsdl:message>
+
+    <wsdl:message name="sayHiResponse">
+        <wsdl:part name="body" element="tns:responseElement"/>
+    </wsdl:message>
+
+    <wsdl:portType name="Greeter">
+        <wsdl:operation name="sayHi">
+            <wsdl:input message="tns:sayHiRequest" name="sayHiRequest"/>
+            <wsdl:output message="tns:sayHiResponse" name="sayHiResponse"/>
+        </wsdl:operation>
+    </wsdl:portType>
+
+    <wsdl:binding name="GreeterBinding" type="tns:Greeter">
+        <soap:binding style="document" 
transport="http://schemas.xmlsoap.org/soap/http"/>
+        <wsdl:operation name="sayHi">
+            <soap:operation soapAction="sayHi"/>
+            <wsdl:input name="sayHiRequest">
+                <soap:body use="literal"/>
+            </wsdl:input>
+            <wsdl:output name="sayHiResponse">
+                <soap:body use="literal"/>
+            </wsdl:output>
+        </wsdl:operation>
+    </wsdl:binding>
+
+    <wsdl:service name="SOAPService">
+        <wsdl:port name="SoapPort" binding="tns:GreeterBinding">
+            <soap:address 
location="http://localhost:9000/SoapContext/SoapPort"/>
+        </wsdl:port>
+    </wsdl:service>
+
+</wsdl:definitions>
diff --git 
a/rt/wsdl/src/test/resources/org/apache/cxf/wsdl11/wsdl_xxe_import_malicious.xsd
 
b/rt/wsdl/src/test/resources/org/apache/cxf/wsdl11/wsdl_xxe_import_malicious.xsd
new file mode 100644
index 00000000000..8c7c66fb9e3
--- /dev/null
+++ 
b/rt/wsdl/src/test/resources/org/apache/cxf/wsdl11/wsdl_xxe_import_malicious.xsd
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE xsd:schema [
+  <!ENTITY xxe_test_file SYSTEM "file:///etc/hostname">
+]>
+<!--
+  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.
+-->
+<!-- Malicious XSD with XXE payload (CWE-611)
+     This document defines an external entity that attempts to read 
/etc/hostname.
+     If the parser is not hardened, the entity reference will be resolved,
+     and the file content will be inlined into the parsed document.
+     This is the document that gets loaded by WSDL4J when processing the 
import.
+-->
+<xsd:schema 
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema";
+    targetNamespace="http://apache.org/xxe_schemas";
+    elementFormDefault="qualified">
+
+    <xsd:element name="payloadElement">
+        <xsd:complexType>
+            <xsd:sequence>
+                <xsd:element name="data" type="xsd:string"/>
+            </xsd:sequence>
+            <xsd:attribute name="content">
+                <xsd:simpleType>
+                    <xsd:restriction base="xsd:string">
+                        <xsd:pattern value="&xxe_test_file;"/>
+                    </xsd:restriction>
+                </xsd:simpleType>
+            </xsd:attribute>
+        </xsd:complexType>
+    </xsd:element>
+
+</xsd:schema>

Reply via email to