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-core.git
commit c06f6ef5517f81f503fef085ae27b0df27a651c9 Author: Robert Lazarski <[email protected]> AuthorDate: Thu Sep 3 05:48:37 2026 -1000 Keep generated output inside the output directory Codegen names its files after the document it was handed: a wsdl:service name reaches the shared sink verbatim, and a QName localPart need not be an NCName, so separators and ".." survive. A WSDL naming its service ../../../src/main/resources/api had the tool write its re-serialized WSDL over the project's real contract file, which later builds then consume. The sink now refuses a name resolving outside the root, which covers every codegen writer rather than the one that was reported. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> --- .../src/org/apache/axis2/util/FileWriter.java | 17 ++++ .../axis2/util/FileWriterContainmentTest.java | 108 +++++++++++++++++++++ src/site/markdown/release-notes/2.0.2.md | 8 ++ 3 files changed, 133 insertions(+) diff --git a/modules/kernel/src/org/apache/axis2/util/FileWriter.java b/modules/kernel/src/org/apache/axis2/util/FileWriter.java index df396a7b17..ec420d5e4f 100644 --- a/modules/kernel/src/org/apache/axis2/util/FileWriter.java +++ b/modules/kernel/src/org/apache/axis2/util/FileWriter.java @@ -63,6 +63,23 @@ public class FileWriter { returnFile = new File(root, fileName); + // Codegen derives file names from the document it was handed -- a + // wsdl:service name reaches here verbatim, and a QName localPart is not + // required to be an NCName, so separators and ".." survive. Every caller is + // writing generated output beneath rootLocation, so a resolved path outside + // it means the name steered the write, not the caller: refuse rather than + // overwrite, say, a project's real contract file with the attacker's. + if (rootLocation != null) { + String canonicalRoot = rootLocation.getCanonicalPath(); + String canonicalTarget = returnFile.getCanonicalPath(); + if (!canonicalTarget.equals(canonicalRoot) + && !canonicalTarget.startsWith(canonicalRoot + File.separator)) { + throw new IOException("Refusing to write generated output outside " + + canonicalRoot + ": the name '" + fileName + + "' resolves to " + canonicalTarget); + } + } + if (!returnFile.exists()) { // returnFile.createNewFile(); } diff --git a/modules/kernel/test/org/apache/axis2/util/FileWriterContainmentTest.java b/modules/kernel/test/org/apache/axis2/util/FileWriterContainmentTest.java new file mode 100644 index 0000000000..cbe4acefeb --- /dev/null +++ b/modules/kernel/test/org/apache/axis2/util/FileWriterContainmentTest.java @@ -0,0 +1,108 @@ +/* + * 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.axis2.util; + +import java.io.File; +import java.io.IOException; + +import junit.framework.TestCase; + +/** + * Code generation derives output file names from the document it was handed: a + * {@code wsdl:service} name reaches the shared sink verbatim, and a QName localPart + * is not required to be an NCName, so path separators and {@code ..} survive. A name + * like {@code ../../../src/main/resources/api} would otherwise have the tool write + * the re-serialized WSDL over a project's real contract file, which later builds + * then consume. + */ +public class FileWriterContainmentTest extends TestCase { + + private File root; + + @Override + protected void setUp() throws Exception { + root = File.createTempFile("axis2-codegen-root", ""); + assertTrue(root.delete()); + assertTrue(root.mkdirs()); + } + + @Override + protected void tearDown() throws Exception { + if (root != null) { + root.delete(); + } + } + + public void testOrdinaryNamesResolveUnderTheRoot() throws Exception { + File created = FileWriter.createClassFile(root, null, "Version", ".wsdl"); + assertEquals(new File(root, "Version.wsdl").getCanonicalPath(), + created.getCanonicalPath()); + } + + public void testPackageDirectoriesStillWork() throws Exception { + File created = FileWriter.createClassFile(root, "com.example.svc", "Stub", ".java"); + assertTrue("the package path must stay under the root", + created.getCanonicalPath().startsWith(root.getCanonicalPath() + File.separator)); + assertTrue(created.getName().equals("Stub.java")); + } + + /** The finding: a traversing service name must not select the write target. */ + public void testATraversingFileNameIsRefused() throws Exception { + try { + FileWriter.createClassFile(root, null, + ".." + File.separator + ".." + File.separator + "api", ".wsdl"); + fail("a name escaping the output directory must be refused"); + } catch (IOException expected) { + assertTrue("the message should say what was refused, was: " + + expected.getMessage(), + expected.getMessage().contains("Refusing to write generated output")); + } + } + + /** + * The package argument cannot traverse, and it is worth recording why rather + * than assuming the guard is what stops it: the name is split on dots, so a + * {@code ..} segment is consumed as two delimiters and never survives as a + * directory name. The guard is still what contains a traversing file name. + */ + public void testATraversingPackageCannotEscapeAnyway() throws Exception { + File created = FileWriter.createClassFile(root, + "a" + File.separator + ".." + File.separator + "..", "Stub", ".java"); + assertTrue("stays under the root: " + created.getCanonicalPath(), + created.getCanonicalPath().startsWith(root.getCanonicalPath() + File.separator)); + } + + /** + * An absolute-looking name is contained by File itself: {@code File(parent, + * child)} resolves the child against the parent even when it starts with a + * separator. Recorded so nobody removes the guard believing this test covered it. + */ + public void testAnAbsoluteNameIsResolvedUnderTheRootAnyway() throws Exception { + File elsewhere = File.createTempFile("axis2-elsewhere", ".wsdl"); + try { + File created = FileWriter.createClassFile(root, null, + elsewhere.getAbsolutePath(), null); + assertTrue("stays under the root: " + created.getCanonicalPath(), + created.getCanonicalPath() + .startsWith(root.getCanonicalPath() + File.separator)); + } finally { + elsewhere.delete(); + } + } +} diff --git a/src/site/markdown/release-notes/2.0.2.md b/src/site/markdown/release-notes/2.0.2.md index df2b962b59..5fce9a269b 100644 --- a/src/site/markdown/release-notes/2.0.2.md +++ b/src/site/markdown/release-notes/2.0.2.md @@ -82,6 +82,14 @@ in `SECURITY.md`. `.xsd` and `.wsdl` names that stay inside META-INF are served now, enforced inside the shared helper so all three callers inherit it. +- **Generated output cannot be written outside the output directory.** Code + generation derives file names from the document it was handed, and a + `wsdl:service` name reached the shared file sink verbatim -- a QName localPart is + not required to be an NCName, so separators and `..` survived. A WSDL naming its + service `../../../src/main/resources/api` had the tool write its re-serialized + WSDL over a project's real contract file. The sink now refuses a name that + resolves outside the output root. + - **Schema values are escaped before they reach generated source.** Element and attribute default and fixed values, enumeration facets and the numeric range facets were copied out of the schema into generated Java and C *inside string literals*
