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*

Reply via email to