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

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

commit 3d4987357c0fc295c6efc857beff1a9c757f12ac
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed Oct 21 07:37:39 2026 +0100

    Disable external DTD access when parsing an XML catalog
---
 .../apache/cxf/maven_plugin/XSDToJavaRunner.java   |  32 ++++++-
 .../apache/cxf/maven_plugin/CatalogXXETest.java    | 103 +++++++++++++++++++++
 2 files changed, 134 insertions(+), 1 deletion(-)

diff --git 
a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XSDToJavaRunner.java 
b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XSDToJavaRunner.java
index 9f37374..991b435 100644
--- 
a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XSDToJavaRunner.java
+++ 
b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XSDToJavaRunner.java
@@ -94,6 +94,36 @@ public class XSDToJavaRunner {
         }
     }
     
+    /**
+     * Parses an XML catalog with external DTD and schema access disabled, so 
that a catalog
+     * cannot be used as an XXE vector (reading local files or making network 
requests while it
+     * is parsed). A legitimate OASIS XML catalog needs neither, so this does 
not restrict normal
+     * use. The restriction is applied only around the catalog parse and then 
restored, so it does
+     * not affect how the schemas themselves are parsed.
+     */
+    static void parseCatalog(CatalogResolver catalogResolver, String 
catalogPath) throws IOException {
+        String dtdKey = "javax.xml.accessExternalDTD";
+        String schemaKey = "javax.xml.accessExternalSchema";
+        String dtd = System.getProperty(dtdKey);
+        String schema = System.getProperty(schemaKey);
+        System.setProperty(dtdKey, "");
+        System.setProperty(schemaKey, "");
+        try {
+            catalogResolver.getCatalog().parseCatalog(catalogPath);
+        } finally {
+            restore(dtdKey, dtd);
+            restore(schemaKey, schema);
+        }
+    }
+
+    private static void restore(String key, String value) {
+        if (value == null) {
+            System.clearProperty(key);
+        } else {
+            System.setProperty(key, value);
+        }
+    }
+
     public int run() throws Exception {
         List<URL> urls = new ArrayList<URL>();
         for (String s : cpList) {
@@ -151,7 +181,7 @@ public class XSDToJavaRunner {
                     if (entityResolver == null) {
                         entityResolver = catResolver;
                     }
-                    
catResolver.getCatalog().parseCatalog(catalogFile.getPath());
+                    parseCatalog(catResolver, catalogFile.getPath());
                 }
             };
 
diff --git 
a/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/CatalogXXETest.java 
b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/CatalogXXETest.java
new file mode 100644
index 0000000..843164c
--- /dev/null
+++ 
b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/CatalogXXETest.java
@@ -0,0 +1,103 @@
+/**
+ * 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.cxf.maven_plugin;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+import org.apache.xml.resolver.CatalogManager;
+import org.apache.xml.resolver.tools.CatalogResolver;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class CatalogXXETest {
+
+    private static final String PROP = "javax.xml.accessExternalDTD";
+
+    @Rule
+    public TemporaryFolder tmp = new TemporaryFolder();
+
+    private CatalogResolver newResolver() {
+        CatalogManager cm = new CatalogManager();
+        cm.setUseStaticCatalog(false);
+        cm.setIgnoreMissingProperties(true);
+        return new CatalogResolver(cm);
+    }
+
+    private File catalog(String name, String content) throws Exception {
+        File f = new File(tmp.getRoot(), name);
+        Files.write(f.toPath(), content.getBytes(StandardCharsets.UTF_8));
+        return f;
+    }
+
+    // a catalog whose external DTD points at a path that does not exist; if 
the parser fetches
+    // it (XXE), parsing fails with FileNotFoundException naming that path
+    private File maliciousCatalog() throws Exception {
+        File probe = new File(tmp.getRoot(), "xxe-probe-does-not-exist.dtd");
+        return catalog("evil.cat",
+            "<?xml version='1.0'?>\n"
+            + "<!DOCTYPE catalog SYSTEM '" + probe.toURI() + "'>\n"
+            + "<catalog xmlns='urn:oasis:names:tc:entity:xmlns:xml:catalog'>"
+            + "<system systemId='urn:x' uri='x.xsd'/></catalog>");
+    }
+
+    @Test
+    public void hardenedParseDoesNotFetchExternalDtd() throws Exception {
+        // must not throw: the external DTD is not fetched
+        XSDToJavaRunner.parseCatalog(newResolver(), 
maliciousCatalog().getPath());
+    }
+
+    @Test
+    public void rawParseWouldFetchExternalDtd() throws Exception {
+        // documents the behaviour the hardening guards against: the 
unhardened parser fetches
+        try {
+            
newResolver().getCatalog().parseCatalog(maliciousCatalog().getPath());
+            // if xml-resolver is ever fixed upstream this may stop fetching; 
then the hardening
+            // is belt-and-braces rather than load-bearing, which is fine
+        } catch (FileNotFoundException expected) {
+            Assert.assertTrue(expected.getMessage(), 
expected.getMessage().contains("xxe-probe-does-not-exist"));
+        }
+    }
+
+    @Test
+    public void legitimateCatalogStillResolves() throws Exception {
+        File mapped = new File(tmp.getRoot(), "real.xsd");
+        Files.write(mapped.toPath(), 
"<schema/>".getBytes(StandardCharsets.UTF_8));
+        File cat = catalog("good.cat",
+            "<?xml version='1.0'?>\n"
+            + "<catalog xmlns='urn:oasis:names:tc:entity:xmlns:xml:catalog'>"
+            + "<system systemId='urn:good' uri='" + mapped.toURI() + 
"'/></catalog>");
+        CatalogResolver resolver = newResolver();
+        XSDToJavaRunner.parseCatalog(resolver, cat.getPath());
+        Assert.assertEquals(mapped.toURI().toString(), 
resolver.getCatalog().resolveSystem("urn:good"));
+    }
+
+    @Test
+    public void propertyIsRestoredAfterParse() throws Exception {
+        String before = System.getProperty(PROP);
+        XSDToJavaRunner.parseCatalog(newResolver(), 
maliciousCatalog().getPath());
+        Assert.assertEquals(before, System.getProperty(PROP));
+    }
+}

Reply via email to