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 3cc039d01296910585086f0471fd9250c3148d71
Author: Robert Lazarski <[email protected]>
AuthorDate: Sat Sep 5 03:22:45 2026 -1000

    Confine packaged-metadata lookup and fix a fail-open schema guard
    
    The ?xsd= and ?wsdl2= queries resolved a requested name with
    getResourceAsStream, which delegates parent-first, so they served META-INF
    schemas from any jar on the classpath while the file routes -- fixed for 
this
    once as AXIS2-5846 -- did not. Both rules now live in one kernel helper,
    MetaInfResources, so the two routes cannot drift apart again. Separately, 
the
    default schema resolver classified schemaLocation with java.net.URI and 
ignored
    URISyntaxException, so a location URI rejects but URL accepts was resolved 
as
    relative and fetched; classification is textual now.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 SECURITY.md                                        |  50 ++++++-
 .../org/apache/axis2/description/AxisService.java  |  20 +--
 .../description/WSDLToAxisServiceBuilder.java      |  64 ++++++--
 .../org/apache/axis2/util/MetaInfResources.java    | 106 +++++++++++++
 .../description/AbsoluteSchemaLocationTest.java    | 110 ++++++++++++++
 .../apache/axis2/util/MetaInfResourcesTest.java    | 166 +++++++++++++++++++++
 .../axis2/transport/http/HTTPTransportUtils.java   |  54 +------
 src/site/markdown/release-notes/2.0.2.md           |  24 +++
 8 files changed, 523 insertions(+), 71 deletions(-)

diff --git a/SECURITY.md b/SECURITY.md
index 41c3f93a79..e529a9b420 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -264,6 +264,19 @@ migration from `commons-fileupload` 1.x to 
`commons-fileupload2` in
    `DefaultURIResolver`; a location that is not absolute is looked up inside
    the archive.
 
+   `WSDLToAxisServiceBuilder` installs a second, separate resolver where a
+   caller supplied none, refusing *every* absolute `schemaLocation` -- `file:`
+   included -- so that only a relative location resolves, against the WSDL's 
own
+   base URI. Until 2.0.2 it classified with `java.net.URI` and swallowed
+   `URISyntaxException`. `URI` enforces RFC 2396 and rejects a location 
holding a
+   space, `|`, `{`, `}`, `^` or a backslash, while the `java.net.URL` the 
delegate
+   builds accepts them, so such a location was never classified and was 
resolved as
+   if relative -- the absolute remote fetch the guard exists to refuse, 
reached by
+   being malformed. Classification is now textual (a scheme prefix, or a `//`
+   network-path reference, which takes the base document's scheme) and cannot 
fail
+   open. A relative name that `URI` also rejects, such as one containing a 
space,
+   still resolves.
+
    `file:` was added to the guard in 2.0.2. Only `AARBasedWSDLLocator` had
    been letting one through: it counts `file:` as absolute, so an absolute
    `file:` import passed the scheme check and fell through to the parent
@@ -404,8 +417,23 @@ migration from `commons-fileupload` 1.x to 
`commons-fileupload2` in
     Separately, the `?xsd=` route reaches a service's packaged META-INF with
     the request's value, and that directory holds `services.xml`, whose
     parameters name keystores and password-callback classes. Only schema and
-    WSDL documents are servable, enforced inside the shared stream helper so
-    every caller inherits it rather than repeating it.
+    WSDL documents are servable, and only from the queried service's own
+    archive: the lookup uses `URLClassLoader.findResource`, which does not
+    delegate, so a name cannot reach a `META-INF` resource in an unrelated jar
+    further up the chain -- Axis2's own jars, `WEB-INF/lib`, or the container's
+    shared libraries. That is the AXIS2-5846 rule, and in 2.0.2 it holds on 
every
+    route: `AxisService.printXSD` and `printWSDL2` had kept using
+    `getResourceAsStream`, which delegates parent-first, so the `?xsd=` and
+    `?wsdl2=` queries searched the whole hierarchy while the file routes did 
not.
+    Both rules now live in one kernel helper, `MetaInfResources`, which the
+    transport delegates to, because two copies of this check are what let the 
two
+    routes drift apart in the first place.
+
+    A classloader that is not a `URLClassLoader` cannot be searched without
+    delegating, so this route serves nothing there rather than serving too 
much.
+    Deployed archives always get a `DeploymentClassLoader`, which is one; an
+    embedder building an `AxisService` against some other classloader loses 
this
+    route and keeps every other way of publishing a schema.
 
     A hidden service is answered exactly as an undeployed one, on every route,
     body included: the query routes no longer send 403 where an absent service
@@ -424,6 +452,24 @@ migration from `commons-fileupload` 1.x to 
`commons-fileupload2` in
     URI, SOAPAction and WS-Addressing binds the service before the Security 
phase
     and is unaffected.
 
+14. **Generated endpoint addresses depend on deployment configuration
+    (documentation, 2.0.2):** Set `httpFrontendHostUrl` in `axis2.xml` -- or an
+    explicit `port` parameter on the servlet transport -- for any deployment 
whose
+    published WSDL is consumed by others. Neither is set by default, and 
without
+    them `AxisServlet` autodetects its port from the first request after 
startup
+    and keeps it for the lifetime of the process. In a standard container
+    `getServerPort()` derives from the client's `Host` header, so the first 
caller
+    after a deployment or restart decides the port written into the 
`soap:address`
+    and EPRs served to every later client. The host part is taken per request 
and
+    reflects only to the requester; the port is shared state.
+
+    This is not fixed in code. Reading the actual listening port instead would
+    return the back-end port and publish a wrong address for every deployment
+    behind a TLS-terminating proxy, which is the common case, and deriving the
+    port per request means changing `TransportListener.getEPRsForService`, 
which
+    `ListenerManager` also calls at startup with no request in scope. Configure
+    the front-end URL; it is the only answer that is correct for both.
+
 ## Reporting Security Issues
 
 Report vulnerabilities to: **[email protected]**
diff --git a/modules/kernel/src/org/apache/axis2/description/AxisService.java 
b/modules/kernel/src/org/apache/axis2/description/AxisService.java
index 4b39449362..531b0372aa 100644
--- a/modules/kernel/src/org/apache/axis2/description/AxisService.java
+++ b/modules/kernel/src/org/apache/axis2/description/AxisService.java
@@ -57,6 +57,7 @@ import org.apache.axis2.i18n.Messages;
 import org.apache.axis2.phaseresolver.PhaseResolver;
 import org.apache.axis2.kernel.TransportListener;
 import org.apache.axis2.util.IOUtils;
+import org.apache.axis2.util.MetaInfResources;
 import org.apache.axis2.util.JavaUtils;
 import org.apache.axis2.util.Loader;
 import org.apache.axis2.util.LoggingControl;
@@ -1397,11 +1398,11 @@ public class AxisService extends AxisDescription {
                 schema.write(new OutputStreamWriter(out, "UTF8"));
                 out.flush();
             } else {
-                // make sure we are only serving .xsd files and ignore 
requests with
-                // ".." in the name.
-                if (xsd.endsWith(".xsd") && xsd.indexOf("..") == -1) {
-                    InputStream in = getClassLoader().getResourceAsStream(
-                            DeploymentConstants.META_INF + "/" + xsd);
+                // Only .xsd files, and only from this service's own archive:
+                // MetaInfResources does not delegate to ancestor 
classloaders, so
+                // the name cannot reach a schema packaged in an unrelated jar.
+                if (xsd.endsWith(".xsd")) {
+                    InputStream in = 
MetaInfResources.getResourceAsStream(this, xsd);
                     if (in != null) {
                         IOUtils.copy(in, out, true);
                     } else {
@@ -1873,11 +1874,10 @@ public class AxisService extends AxisDescription {
         // if the wsdl2 parameter is not empty or null in the requested URL, 
get the wsdl  from the META-INF and serve.
         //else construct the wsdl out of axis service and serve.
         if ((wsdl != null ) && (!"".equals(wsdl))) {
-            // make sure we are only serving .wsdl files and ignore requests 
with
-            // ".." in the name.
-            if (wsdl.endsWith(".wsdl") && wsdl.indexOf("..") == -1) {
-                InputStream in = getClassLoader().getResourceAsStream(
-                                    DeploymentConstants.META_INF + "/" + wsdl);
+            // Only .wsdl files, and only from this service's own archive; see 
the
+            // note on the ?xsd= route above.
+            if (wsdl.endsWith(".wsdl")) {
+                InputStream in = MetaInfResources.getResourceAsStream(this, 
wsdl);
                 if (in != null) {
                     IOUtils.copy(in, out, true);
                 } else {
diff --git 
a/modules/kernel/src/org/apache/axis2/description/WSDLToAxisServiceBuilder.java 
b/modules/kernel/src/org/apache/axis2/description/WSDLToAxisServiceBuilder.java
index 560f4ea544..805e28f006 100644
--- 
a/modules/kernel/src/org/apache/axis2/description/WSDLToAxisServiceBuilder.java
+++ 
b/modules/kernel/src/org/apache/axis2/description/WSDLToAxisServiceBuilder.java
@@ -146,8 +146,10 @@ public abstract class WSDLToAxisServiceBuilder {
             // resolution (SSRF via absolute schemaLocation URLs). The
             // default URIResolver in xmlschema-core follows any absolute
             // URI including http://, https://, ftp://, and jar://.
-            // Local file:// and relative paths are allowed for co-packaged
-            // schemas in .aar/.war deployments.
+            // Every absolute location is refused, file:// included; only a
+            // relative path resolves, against the base URI of the WSDL
+            // document, which is how co-packaged schemas in .aar/.war
+            // deployments refer to each other.
             schemaCollection.setSchemaResolver(
                 new org.apache.ws.commons.schema.resolver.URIResolver() {
                     private final 
org.apache.ws.commons.schema.resolver.DefaultURIResolver
@@ -158,17 +160,11 @@ public abstract class WSDLToAxisServiceBuilder {
                         // SSRF and LFI. Relative paths (e.g., "wsat.xsd")
                         // are safe — they resolve against the local base
                         // URI of the WSDL document.
-                        if (loc != null) {
-                            try {
-                                java.net.URI locUri = new java.net.URI(loc);
-                                if (locUri.isAbsolute()) {
-                                    throw new RuntimeException(
-                                        "Absolute schemaLocation blocked: "
-                                        + loc + " (use setCustomResolver"
-                                        + " to opt in)");
-                                }
-                            } catch (java.net.URISyntaxException ignored) {
-                            }
+                        if (isAbsoluteSchemaLocation(loc)) {
+                            throw new RuntimeException(
+                                "Absolute schemaLocation blocked: "
+                                + loc + " (use setCustomResolver"
+                                + " to opt in)");
                         }
                         return delegate.resolveEntity(ns, loc, base);
                     }
@@ -178,6 +174,48 @@ public abstract class WSDLToAxisServiceBuilder {
         return schemaCollection.read(element);
     }
 
+    /**
+     * Whether a schemaLocation names an absolute location, and so must not be
+     * resolved by the default resolver.
+     * <p>
+     * Classified textually rather than with {@link java.net.URI}, which is 
what this
+     * used to do. {@code URI} enforces RFC 2396 and throws on a location 
containing a
+     * space or another illegal character, while {@code java.net.URL} -- which 
the
+     * delegate builds -- accepts many of those. A {@code URISyntaxException} 
therefore
+     * meant "unclassified", and the guard caught it, treated the location as 
relative
+     * and resolved it: exactly the absolute remote locations it exists to 
refuse got
+     * through, by being malformed. A guard that cannot classify its input 
must not
+     * pass it.
+     *
+     * @param loc the schemaLocation as the document gave it
+     * @return true if it carries a scheme, or is a network-path reference
+     */
+    static boolean isAbsoluteSchemaLocation(String loc) {
+        if (loc == null) {
+            return false;
+        }
+        String trimmed = loc.trim();
+        // A network-path reference inherits the base document's scheme, so it 
is
+        // remote whenever the base is.
+        if (trimmed.startsWith("//")) {
+            return true;
+        }
+        // RFC 3986 scheme: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) ":"
+        for (int i = 0; i < trimmed.length(); i++) {
+            char c = trimmed.charAt(i);
+            if (c == ':') {
+                return i > 0;
+            }
+            boolean schemeChar = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 
'Z')
+                    || (i > 0 && ((c >= '0' && c <= '9')
+                            || c == '+' || c == '-' || c == '.'));
+            if (!schemeChar) {
+                return false;
+            }
+        }
+        return false;
+    }
+
     /**
      * Find the XML schema prefix
      *
diff --git a/modules/kernel/src/org/apache/axis2/util/MetaInfResources.java 
b/modules/kernel/src/org/apache/axis2/util/MetaInfResources.java
new file mode 100644
index 0000000000..4fc221f6a4
--- /dev/null
+++ b/modules/kernel/src/org/apache/axis2/util/MetaInfResources.java
@@ -0,0 +1,106 @@
+/*
+ * 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.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Locale;
+
+import org.apache.axis2.description.AxisService;
+
+/**
+ * Serves a schema or WSDL document that a service archive packages under
+ * {@code META-INF/}, by the name the request asked for.
+ * <p>
+ * Two properties have to hold on every route that does this, and they used to 
hold on
+ * some and not others, which is why the lookup lives in one place now:
+ * <ul>
+ * <li><b>Only schema and WSDL documents.</b> A service archive's {@code 
META-INF} holds
+ *     more than those -- {@code services.xml}, whose parameters name 
keystores and
+ *     password-callback classes, plus {@code MANIFEST.MF} and module 
policies.</li>
+ * <li><b>Only the queried service's own archive.</b> The lookup uses
+ *     {@link URLClassLoader#findResource}, which does not delegate to the 
parent, so a
+ *     request cannot reach a {@code META-INF} resource in an unrelated jar 
further up
+ *     the chain -- Axis2's own jars, {@code WEB-INF/lib}, or the container's 
shared
+ *     libraries. That is the AXIS2-5846 rule.</li>
+ * </ul>
+ * A classloader that is not a {@link URLClassLoader} cannot be searched 
without
+ * delegating, so nothing is served in that case rather than serving too much. 
Deployed
+ * archives always get a {@code DeploymentClassLoader}, which is one; an 
embedder that
+ * builds an {@code AxisService} against some other classloader loses this 
route and
+ * keeps every other way of publishing a schema.
+ */
+public class MetaInfResources {
+
+    private MetaInfResources() {
+    }
+
+    /**
+     * Whether a requested name may be served at all: a named {@code .xsd} or
+     * {@code .wsdl}, with no traversal, no scheme and no absolute path.
+     *
+     * @param name the name as the request gave it
+     * @return true if it is a document this route is allowed to serve
+     */
+    public static boolean isServable(String name) {
+        if (name == null || name.isEmpty()) {
+            return false;
+        }
+        String lower = name.toLowerCase(Locale.ENGLISH);
+        int extension = lower.endsWith(".xsd") ? 4 : (lower.endsWith(".wsdl") 
? 5 : 0);
+        if (extension == 0) {
+            return false;
+        }
+        // Require something to be named, so that a bare ".xsd" is not a 
document.
+        String base = name.substring(0, name.length() - extension);
+        if (base.isEmpty() || base.endsWith("/")) {
+            return false;
+        }
+        return name.indexOf("..") < 0 && name.indexOf(':') < 0 && 
!name.startsWith("/");
+    }
+
+    /**
+     * Opens a packaged document from the service's own archive.
+     *
+     * @param service the service whose archive is being asked
+     * @param name    the document name as the request gave it
+     * @return a stream over the document, or null if it is not servable, not 
present,
+     *         or the service's classloader cannot be searched without 
delegating
+     */
+    public static InputStream getResourceAsStream(AxisService service, String 
name) {
+        if (service == null || !isServable(name)) {
+            return null;
+        }
+        ClassLoader classLoader = service.getClassLoader();
+        if (!(classLoader instanceof URLClassLoader)) {
+            return null;
+        }
+        URL url = ((URLClassLoader) classLoader).findResource("META-INF/" + 
name);
+        if (url == null) {
+            return null;
+        }
+        try {
+            return url.openStream();
+        } catch (IOException ex) {
+            return null;
+        }
+    }
+}
diff --git 
a/modules/kernel/test/org/apache/axis2/description/AbsoluteSchemaLocationTest.java
 
b/modules/kernel/test/org/apache/axis2/description/AbsoluteSchemaLocationTest.java
new file mode 100644
index 0000000000..298591ba92
--- /dev/null
+++ 
b/modules/kernel/test/org/apache/axis2/description/AbsoluteSchemaLocationTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.description;
+
+import junit.framework.TestCase;
+
+/**
+ * The default schema resolver refuses an absolute schemaLocation. It decided 
what was
+ * absolute with java.net.URI and swallowed URISyntaxException, so a location 
URI
+ * rejects but java.net.URL accepts -- one containing a space, say -- was 
classified as
+ * relative and fetched. These pin the classification, including the cases 
that used to
+ * be unclassified.
+ */
+public class AbsoluteSchemaLocationTest extends TestCase {
+
+    private void assertAbsolute(String loc) {
+        assertTrue("should be refused: " + loc,
+                WSDLToAxisServiceBuilder.isAbsoluteSchemaLocation(loc));
+    }
+
+    private void assertRelative(String loc) {
+        assertFalse("should still resolve: " + loc,
+                WSDLToAxisServiceBuilder.isAbsoluteSchemaLocation(loc));
+    }
+
+    public void testOrdinaryAbsoluteLocationsAreRefused() {
+        assertAbsolute("http://internal-host/schema.xsd";);
+        assertAbsolute("https://internal-host/schema.xsd";);
+        assertAbsolute("ftp://internal-host/schema.xsd";);
+        assertAbsolute("jar:file:/tmp/a.jar!/schema.xsd");
+        assertAbsolute("file:///etc/passwd");
+    }
+
+    /**
+     * The bypass: java.net.URI throws on the space, so the old guard caught, 
gave up
+     * on classifying, and resolved it anyway.
+     */
+    public void testAnAbsoluteLocationWithAUriIllegalCharacterIsStillRefused() 
{
+        assertAbsolute("http://169.254.169.254/latest/meta-data/iam a.xsd");
+        assertAbsolute("http://internal-host/a|b.xsd");
+        assertAbsolute("http://internal-host/a{b}.xsd";);
+        assertAbsolute("http://internal-host/a^b.xsd";);
+        assertAbsolute("http://internal-host/a\\b.xsd";);
+    }
+
+    /** Leading whitespace must not hide the scheme either. */
+    public void testWhitespaceDoesNotHideAScheme() {
+        assertAbsolute("  http://internal-host/schema.xsd";);
+        assertAbsolute("\thttps://internal-host/schema.xsd";);
+    }
+
+    /** A network-path reference takes the base document's scheme. */
+    public void testANetworkPathReferenceIsRefused() {
+        assertAbsolute("//internal-host/schema.xsd");
+    }
+
+    /** The ordinary co-packaged case must keep working; that is the whole 
point. */
+    public void testRelativeLocationsStillResolve() {
+        assertRelative("wsat.xsd");
+        assertRelative("./wsat.xsd");
+        assertRelative("../common/wsat.xsd");
+        assertRelative("schemas/wsat.xsd");
+        assertRelative("/absolute/path/wsat.xsd");
+        assertRelative(null);
+        assertRelative("");
+    }
+
+    /**
+     * A relative name that java.net.URI also rejects. It resolved before and 
must
+     * keep resolving: failing closed on everything URI dislikes would have 
broken
+     * ordinary deployments rather than the attack.
+     */
+    public void testARelativeNameWithASpaceStillResolves() {
+        assertRelative("my schema.xsd");
+        assertRelative("schemas/my schema.xsd");
+    }
+
+    /**
+     * A Windows absolute path is refused. It parses as a one-letter scheme, 
and the
+     * forward-slash form "C:/schemas/a.xsd" was already refused; only the 
backslash
+     * form slipped through, by being URI-invalid. An absolute local path is 
the LFI
+     * case this guard is for, so the two forms agreeing is the fix, not a 
casualty.
+     */
+    public void testAWindowsAbsolutePathIsRefusedInBothForms() {
+        assertAbsolute("C:/schemas/a.xsd");
+        assertAbsolute("C:\\schemas\\a.xsd");
+    }
+
+    /** A scheme needs a leading letter, so these are names, not locations. */
+    public void testThingsThatOnlyLookLikeSchemes() {
+        assertRelative("2foo:bar.xsd");
+        assertRelative(":leading-colon.xsd");
+    }
+}
diff --git 
a/modules/kernel/test/org/apache/axis2/util/MetaInfResourcesTest.java 
b/modules/kernel/test/org/apache/axis2/util/MetaInfResourcesTest.java
new file mode 100644
index 0000000000..3256134bbc
--- /dev/null
+++ b/modules/kernel/test/org/apache/axis2/util/MetaInfResourcesTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.FileOutputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+
+import junit.framework.TestCase;
+
+import org.apache.axis2.description.AxisService;
+
+/**
+ * The ?xsd= and ?wsdl2= routes serve a packaged document by the name the 
request
+ * asked for. Two rules hold: only schema and WSDL documents, and only from the
+ * queried service's own archive -- the AXIS2-5846 rule, which the transport 
route
+ * followed and AxisService.printXSD/printWSDL2 did not, since they used
+ * getResourceAsStream and so searched the whole ancestor chain.
+ */
+public class MetaInfResourcesTest extends TestCase {
+
+    private File root;
+    private File parentRoot;
+    private URLClassLoader parent;
+    private URLClassLoader loader;
+
+    @Override
+    protected void setUp() throws Exception {
+        root = makeArchive("own", "mine.xsd", "<own/>");
+        parentRoot = makeArchive("parent", "theirs.xsd", "<theirs/>");
+        parent = new URLClassLoader(new URL[] { parentRoot.toURI().toURL() }, 
null);
+        loader = new URLClassLoader(new URL[] { root.toURI().toURL() }, 
parent);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (loader != null) {
+            loader.close();
+        }
+        if (parent != null) {
+            parent.close();
+        }
+        delete(root);
+        delete(parentRoot);
+    }
+
+    private File makeArchive(String prefix, String name, String content) 
throws Exception {
+        File dir = File.createTempFile("axis2-" + prefix, "");
+        dir.delete();
+        File metaInf = new File(dir, "META-INF");
+        assertTrue(metaInf.mkdirs());
+        FileOutputStream out = new FileOutputStream(new File(metaInf, name));
+        try {
+            out.write(content.getBytes(StandardCharsets.UTF_8));
+        } finally {
+            out.close();
+        }
+        return dir;
+    }
+
+    private void delete(File file) {
+        if (file == null) {
+            return;
+        }
+        File[] children = file.listFiles();
+        if (children != null) {
+            for (int i = 0; i < children.length; i++) {
+                delete(children[i]);
+            }
+        }
+        file.delete();
+    }
+
+    private AxisService serviceOn(ClassLoader classLoader) {
+        AxisService service = new AxisService("Test");
+        service.setClassLoader(classLoader);
+        return service;
+    }
+
+    private String read(InputStream in) throws Exception {
+        assertNotNull(in);
+        try {
+            byte[] buffer = new byte[256];
+            int n = in.read(buffer);
+            return new String(buffer, 0, n, StandardCharsets.UTF_8);
+        } finally {
+            in.close();
+        }
+    }
+
+    public void testTheServicesOwnDocumentIsServed() throws Exception {
+        InputStream in = 
MetaInfResources.getResourceAsStream(serviceOn(loader), "mine.xsd");
+        assertEquals("<own/>", read(in));
+    }
+
+    /** The finding: getResourceAsStream delegates parent-first and would find 
this. */
+    public void testADocumentInAnAncestorClassloaderIsNotServed() throws 
Exception {
+        assertNotNull("the ancestor really does carry it, so the test means 
something",
+                loader.getResourceAsStream("META-INF/theirs.xsd"));
+        assertNull("a resource from an unrelated jar must not be reachable by 
name",
+                MetaInfResources.getResourceAsStream(serviceOn(loader), 
"theirs.xsd"));
+    }
+
+    public void testAMissingDocumentIsNotFound() {
+        assertNull(MetaInfResources.getResourceAsStream(serviceOn(loader), 
"absent.xsd"));
+    }
+
+    /**
+     * A classloader that is not a URLClassLoader cannot be searched without
+     * delegating, so this route serves nothing rather than serving the whole 
chain.
+     */
+    public void testANonUrlClassloaderServesNothing() {
+        ClassLoader plain = new ClassLoader(loader) {
+        };
+        assertNull(MetaInfResources.getResourceAsStream(serviceOn(plain), 
"mine.xsd"));
+    }
+
+    public void testANullServiceIsHandled() {
+        assertNull(MetaInfResources.getResourceAsStream(null, "mine.xsd"));
+    }
+
+    public void testOnlySchemaAndWsdlDocumentsAreServable() {
+        assertTrue(MetaInfResources.isServable("a.xsd"));
+        assertTrue(MetaInfResources.isServable("a.wsdl"));
+        assertTrue(MetaInfResources.isServable("sub/a.xsd"));
+        assertFalse("services.xml names keystores and callback classes",
+                MetaInfResources.isServable("services.xml"));
+        assertFalse(MetaInfResources.isServable("MANIFEST.MF"));
+        assertFalse(MetaInfResources.isServable(null));
+        assertFalse(MetaInfResources.isServable(""));
+    }
+
+    public void testTraversalAbsoluteAndSchemeNamesAreRefused() {
+        assertFalse(MetaInfResources.isServable("../../services.xml"));
+        assertFalse(MetaInfResources.isServable("../a.xsd"));
+        assertFalse(MetaInfResources.isServable("/etc/a.xsd"));
+        assertFalse(MetaInfResources.isServable("http://elsewhere/a.xsd";));
+        assertFalse("a bare extension is not a document", 
MetaInfResources.isServable(".xsd"));
+        assertFalse(MetaInfResources.isServable("sub/.wsdl"));
+    }
+
+    /** Traversal is refused whatever the archive holds, not merely not found. 
*/
+    public void testTraversalIsRefusedBeforeAnyLookup() throws Exception {
+        assertNull(MetaInfResources.getResourceAsStream(
+                serviceOn(loader), "../META-INF/mine.xsd"));
+    }
+}
diff --git 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportUtils.java
 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportUtils.java
index f33d36f479..86abb8c73f 100644
--- 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportUtils.java
+++ 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportUtils.java
@@ -37,6 +37,7 @@ import org.apache.axis2.builder.BuilderUtil;
 import org.apache.axis2.context.ConfigurationContext;
 import org.apache.axis2.context.MessageContext;
 import org.apache.axis2.description.AxisService;
+import org.apache.axis2.util.MetaInfResources;
 import org.apache.axis2.description.Parameter;
 import org.apache.axis2.description.TransportInDescription;
 import org.apache.axis2.engine.AxisConfiguration;
@@ -53,11 +54,8 @@ import javax.xml.parsers.FactoryConfigurationError;
 import javax.xml.stream.XMLStreamException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Locale;
 import java.io.OutputStream;
 import java.net.SocketException;
-import java.net.URL;
-import java.net.URLClassLoader;
 import java.time.LocalDateTime;
 import java.util.Iterator;
 import java.util.Map;
@@ -455,52 +453,16 @@ public class HTTPTransportUtils {
     }
 
     static InputStream getMetaInfResourceAsStream(AxisService service, String 
name) {
-        // Only the packaged schema and WSDL documents are servable here. One 
caller,
-        // the ?xsd= route, reaches this with the request's value verbatim, 
and a
-        // service archive's META-INF holds more than schemas: services.xml, 
whose
-        // parameters name keystores and password-callback classes, plus 
MANIFEST.MF
-        // and module policies. Guarding inside this method rather than at 
each call
-        // site means a future caller inherits the restriction instead of 
having to
-        // remember it.
-        if (!isServableMetadataResource(name)) {
-            return null;
-        }
-        ClassLoader classLoader = service.getClassLoader();
-        if (classLoader instanceof URLClassLoader) {
-            // Only search the service class loader and skip searching the 
ancestors to
-            // avoid local file inclusion vulnerabilities such as AXIS2-5846.
-            URL url = ((URLClassLoader)classLoader).findResource("META-INF/" + 
name);
-            try {
-                return url == null ? null : url.openStream();
-            } catch (IOException ex) {
-                return null;
-            }
-        } else {
-            return null;
-        }
+        // The lookup itself lives in the kernel, because AxisService.printXSD 
and
+        // printWSDL2 serve the same documents by name and have to obey the 
same two
+        // rules -- schemas and WSDLs only, and only from the queried 
service's own
+        // archive (AXIS2-5846). Keeping one implementation is what stops 
those two
+        // routes from drifting apart again.
+        return MetaInfResources.getResourceAsStream(service, name);
     }
 
-    /**
-     * Whether a request-supplied META-INF resource name may be served.
-     *
-     * @param name the resource name, relative to META-INF
-     * @return true only for a schema or WSDL document that stays inside 
META-INF
-     */
     static boolean isServableMetadataResource(String name) {
-        if (name == null || name.isEmpty()) {
-            return false;
-        }
-        String lower = name.toLowerCase(Locale.ENGLISH);
-        int extension = lower.endsWith(".xsd") ? 4 : (lower.endsWith(".wsdl") 
? 5 : 0);
-        if (extension == 0) {
-            return false;
-        }
-        // Require something to be named, so that a bare ".xsd" is not a 
document.
-        String base = name.substring(0, name.length() - extension);
-        if (base.isEmpty() || base.endsWith("/")) {
-            return false;
-        }
-        return name.indexOf("..") < 0 && name.indexOf(':') < 0 && 
!name.startsWith("/");
+        return MetaInfResources.isServable(name);
     }
 
     /**
diff --git a/src/site/markdown/release-notes/2.0.2.md 
b/src/site/markdown/release-notes/2.0.2.md
index 905e9210c1..be07704afb 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -164,6 +164,30 @@ in `SECURITY.md`.
   much as to SOAP faults. The reason is now generic unless details are 
enabled. The
   error is logged with its stack as before.
 
+- **Packaged schema and WSDL documents are served only from the requesting 
service's
+  own archive.** `AxisService.printXSD` and `printWSDL2` -- the `?xsd=` and 
`?wsdl2=`
+  queries -- looked a requested name up with `getResourceAsStream`, which 
delegates
+  parent-first, so an anonymous caller could read any `META-INF/*.xsd` or 
`*.wsdl` from
+  jars anywhere on the webapp or container classpath and fingerprint what was 
deployed.
+  The file routes had been fixed for this once (AXIS2-5846); the query routes 
had not.
+  Both rules -- schemas and WSDLs only, own archive only -- now live in one 
kernel
+  helper, `MetaInfResources`. A service whose classloader is not a 
`URLClassLoader`
+  cannot serve this route at all; deployed archives are unaffected.
+
+- **The absolute-`schemaLocation` guard no longer fails open on malformed 
input.**
+  `WSDLToAxisServiceBuilder`'s default schema resolver classified locations 
with
+  `java.net.URI` and ignored `URISyntaxException`. A location `URI` rejects but
+  `java.net.URL` accepts -- one containing a space, `|`, `{`, `}`, `^` or a 
backslash --
+  was therefore never classified and was resolved as though relative, fetching 
the
+  remote schema the guard exists to refuse. Classification is textual now. 
Relative
+  names that `URI` also rejects still resolve.
+
+- **`httpFrontendHostUrl` is documented as a requirement, not a convenience.** 
Without
+  it (or an explicit servlet-transport `port`), the port advertised in 
generated WSDL
+  comes from the first request's `Host` header after startup and is kept for 
the life of
+  the process. See item 14 of `SECURITY.md`; this is a configuration note, not 
a code
+  change.
+
 - **Two threat-model claims made true, and their limits stated.** The document 
said
   every parser factory the framework creates disables DTDs and external 
entities, and
   that the AAR/WAR resolvers block `file:` resolution. Neither held. The SAAJ 
and

Reply via email to