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)); + } +}
