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 8a81216b1b723e98a3c801de939201ad68f59b23
Author: Robert Lazarski <[email protected]>
AuthorDate: Thu Sep 3 04:47:07 2026 -1000

    Honour exposeServiceMetadata on the listing, WS-MEX and ping
    
    The check was a private method in each HTTP transport, so the modules that 
also
    answer anonymous metadata requests could not reach it and did not apply it: 
the
    /services/ listing named hidden services with their EPRs and operations, 
WS-MEX
    GetMetadata returned their WSDL, schema and policy, and a bodyless ping 
returned
    every operation with its live status. The gate is now AxisService, which 
all of
    them can consult. Separately the ?xsd= route reached any packaged META-INF
    resource, services.xml among them, so the shared stream helper now serves 
only
    schema and WSDL names that stay inside META-INF.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 SECURITY.md                                        | 26 +++++++--
 .../org/apache/axis2/description/AxisService.java  | 28 ++++++++++
 .../apache/axis2/description/AxisServiceTest.java  | 17 ++++++
 .../org/apache/axis2/mex/MexMessageReceiver.java   |  9 ++++
 .../org/apache/axis2/ping/PingMessageReceiver.java | 26 +++++++--
 .../transport/http/HTTPTransportReceiver.java      |  5 +-
 .../axis2/transport/http/HTTPTransportUtils.java   | 34 ++++++++++++
 .../apache/axis2/transport/http/HTTPWorker.java    |  7 +--
 .../apache/axis2/transport/http/ListingAgent.java  | 16 +++---
 .../transport/http/MetaInfResourceGuardTest.java   | 62 ++++++++++++++++++++++
 src/site/markdown/release-notes/2.0.2.md           | 15 ++++--
 11 files changed, 219 insertions(+), 26 deletions(-)

diff --git a/SECURITY.md b/SECURITY.md
index 7100641f27..825b5f5eac 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -315,10 +315,28 @@ migration from `commons-fileupload` 1.x to 
`commons-fileupload2` in
 12. **Uniform metadata exposure (2.0.2):** `exposeServiceMetadata` is now
     honoured by every anonymous metadata route: the `?wsdl`, `?wsdl2` and
     `?xsd` queries as before, plus the `.xsd`/`.wsdl` file routes on both the
-    servlet and standalone HTTP paths, the named-WSDL route, and the
-    OpenAPI/Swagger/MCP generators. A service with exposure disabled is
-    skipped rather than refused, so it stays indistinguishable from one that
-    is not deployed.
+    servlet and standalone HTTP paths, the named-WSDL route, the
+    OpenAPI/Swagger/MCP generators, the `/services/` listing on both render
+    paths, WS-MEX `GetMetadata`, and the ping module's service-level ping. A
+    service with exposure disabled is skipped rather than refused, so it stays
+    indistinguishable from one that is not deployed.
+
+    The gate is `AxisService.isMetadataExposed()`. It lives in the kernel
+    because modules answer anonymous metadata requests too and cannot reach a
+    transport's private copy of the check -- which is how the listing, WS-MEX
+    and ping came to be exempt from a control the query routes enforced.
+
+    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.
+
+    Known gap: the `?wsdl`/`?wsdl2`/`?xsd`/`?policy` query routes still answer
+    403 for a hidden service where an undeployed one gets 404, which is an
+    existence oracle. The file routes already answer alike. Tracked for a
+    follow-up; RFC 9110 section 15.5.4 sanctions answering 404 to conceal a
+    forbidden resource's existence.
 
 13. **Content-based service dispatch (2.0.2):** The inflow phase order is
     Transport, Addressing, Security, PreDispatch, Dispatch, and `DispatchPhase`
diff --git a/modules/kernel/src/org/apache/axis2/description/AxisService.java 
b/modules/kernel/src/org/apache/axis2/description/AxisService.java
index 5fcfeb6a44..4b39449362 100644
--- a/modules/kernel/src/org/apache/axis2/description/AxisService.java
+++ b/modules/kernel/src/org/apache/axis2/description/AxisService.java
@@ -2919,6 +2919,34 @@ public class AxisService extends AxisDescription {
         this.clientSide = clientSide;
     }
 
+    /**
+     * Name of the service parameter that withholds this service's metadata 
from
+     * anonymous callers.
+     */
+    public static final String EXPOSE_SERVICE_METADATA = 
"exposeServiceMetadata";
+
+    /**
+     * Whether this service's metadata may be handed to an anonymous caller.
+     * <p>
+     * Every anonymous channel that describes a service -- the {@code ?wsdl},
+     * {@code ?wsdl2}, {@code ?xsd} and {@code ?policy} routes, the {@code 
.wsdl} and
+     * {@code .xsd} file routes, the service listing, the OpenAPI and MCP 
generators,
+     * WS-MEX and the ping module -- has to consult this before answering, or 
the
+     * operator's decision to hide a service holds on some routes and not 
others.
+     * That is why it lives here rather than being re-implemented per 
transport: the
+     * modules cannot reach a transport's private copy.
+     * <p>
+     * A hidden service should be answered as though it were not deployed, 
rather
+     * than refused, so that the answer does not confirm it exists.
+     *
+     * @return true unless the {@code exposeServiceMetadata} parameter is 
explicitly false
+     */
+    public boolean isMetadataExposed() {
+        Parameter exposeServiceMetadata = 
getParameter(EXPOSE_SERVICE_METADATA);
+        return exposeServiceMetadata == null
+                || 
!JavaUtils.isFalseExplicitly(exposeServiceMetadata.getValue());
+    }
+
     public boolean isElementFormDefault() {
         return elementFormDefault;
     }
diff --git 
a/modules/kernel/test/org/apache/axis2/description/AxisServiceTest.java 
b/modules/kernel/test/org/apache/axis2/description/AxisServiceTest.java
index bf405afb85..e9292dd67c 100644
--- a/modules/kernel/test/org/apache/axis2/description/AxisServiceTest.java
+++ b/modules/kernel/test/org/apache/axis2/description/AxisServiceTest.java
@@ -325,4 +325,21 @@ public class AxisServiceTest extends XMLSchemaTest {
         }
         
     }
+
+    /**
+     * The gate every anonymous metadata channel consults -- the 
?wsdl/?xsd/?policy
+     * routes, the service listing, the OpenAPI and MCP generators, WS-MEX and 
ping.
+     * Exposure is the default; only an explicit false withholds.
+     */
+    public void testMetadataIsExposedUnlessExplicitlyDisabled() throws 
Exception {
+        AxisService service = new AxisService("Hidden");
+        assertTrue("metadata is exposed when the parameter is absent",
+                service.isMetadataExposed());
+
+        service.addParameter(AxisService.EXPOSE_SERVICE_METADATA, "false");
+        assertFalse("an explicit false withholds metadata", 
service.isMetadataExposed());
+
+        service.addParameter(AxisService.EXPOSE_SERVICE_METADATA, "true");
+        assertTrue("an explicit true exposes metadata", 
service.isMetadataExposed());
+    }
 }
diff --git a/modules/mex/src/org/apache/axis2/mex/MexMessageReceiver.java 
b/modules/mex/src/org/apache/axis2/mex/MexMessageReceiver.java
index 35d1c4ae74..a63d2035a8 100644
--- a/modules/mex/src/org/apache/axis2/mex/MexMessageReceiver.java
+++ b/modules/mex/src/org/apache/axis2/mex/MexMessageReceiver.java
@@ -65,6 +65,15 @@ public class MexMessageReceiver extends 
AbstractInOutMessageReceiver {
                serviceConfigMEXParm = 
theService.getParameter(MexConstants.MEX_CONFIG.MEX_PARM);
                
                check_MEX_disabled(serviceConfigMEXParm);
+
+               // WS-MEX GetMetadata returns the WSDL, schema and policy of 
the service --
+               // with no Dialect it returns all three -- so it is one of the 
anonymous
+               // metadata channels exposeServiceMetadata governs. Answered 
exactly as a
+               // MEX-disabled service is, so a hidden service is not 
distinguishable.
+               if (!theService.isMetadataExposed()) {
+                       throw new MexDisabledException(
+                                       "'metadataexchange' parameter 
configured to disable MEX for the service.");
+               }
                
                try {
                        Metadata metadata = handleRequest(msgContext);
diff --git a/modules/ping/src/org/apache/axis2/ping/PingMessageReceiver.java 
b/modules/ping/src/org/apache/axis2/ping/PingMessageReceiver.java
index 65b1c3be86..2b5e6bf1a6 100644
--- a/modules/ping/src/org/apache/axis2/ping/PingMessageReceiver.java
+++ b/modules/ping/src/org/apache/axis2/ping/PingMessageReceiver.java
@@ -37,6 +37,10 @@ import java.util.Iterator;
 public class PingMessageReceiver extends AbstractInOutMessageReceiver 
implements PingConstants {
     private static Log log = LogFactory.getLog(PingMessageReceiver.class);
 
+    /** Detail-free answer, so that no ping response reveals what exists. */
+    private static final String PING_NOT_AVAILABLE =
+            "Ping is not available for the requested target";
+
     public void invokeBusinessLogic(MessageContext inMessage, MessageContext 
outMessage) throws AxisFault {
 
         try {
@@ -107,16 +111,28 @@ public class PingMessageReceiver extends 
AbstractInOutMessageReceiver implements
                 if (axisOperation != null) {
                     operationList.add(axisOperation);
                 } else {
-                    String msg = "Operation not found: " + operationName +
-                            " specified in the ping request for the service" +
-                            inMessage.getAxisService().getName();
-                    log.error(msg);
-                    throw new AxisFault(msg);
+                    // The fault detail used to name the operation and the 
service,
+                    // telling an anonymous caller which operation names 
exist. Log
+                    // it, and answer with the same wording used everywhere 
else.
+                    log.error("Operation not found: " + operationName
+                            + " specified in the ping request for the service "
+                            + inMessage.getAxisService().getName());
+                    throw new AxisFault(PING_NOT_AVAILABLE);
                 }
             }
             operationsIterator = operationList.iterator();
         } else {
             //No operation is mentioned in the request.. So this is a service 
level ping
+            // which answers with every operation name and its live status -- 
the
+            // enumeration exposeServiceMetadata withholds from the other 
anonymous
+            // channels. Answered exactly as an unknown operation is, so a 
hidden
+            // service stays indistinguishable from one that is not deployed.
+            if (!inMessage.getAxisService().isMetadataExposed()) {
+                log.error("Service level ping refused for "
+                        + inMessage.getAxisService().getName()
+                        + ": metadata exposure is disabled");
+                throw new AxisFault(PING_NOT_AVAILABLE);
+            }
             operationsIterator = inMessage.getAxisService().getOperations();
         }
         return operationsIterator;
diff --git 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportReceiver.java
 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportReceiver.java
index 6713a72cbc..f4f7804275 100644
--- 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportReceiver.java
+++ 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportReceiver.java
@@ -114,7 +114,10 @@ public class HTTPTransportReceiver {
 
                 AxisService axisService = (AxisService) it.next();
 
-                if (!Utils.isHiddenService(axisService)) {
+                // isMetadataExposed is a separate control from 
isHiddenService:
+                // a service hidden from the metadata routes must not be named,
+                // EPR'd and have its operations listed here instead.
+                if (!Utils.isHiddenService(axisService) && 
axisService.isMetadataExposed()) {
                     Iterator iterator = axisService.getOperations();
 
                     temp += "<h3><a href=\"" + axisService.getName() + 
"?wsdl\">" +
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 c60625c377..f33d36f479 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
@@ -53,6 +53,7 @@ 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;
@@ -454,6 +455,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
@@ -469,6 +480,29 @@ public class HTTPTransportUtils {
         }
     }
 
+    /**
+     * 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("/");
+    }
+
     /**
      * Simple charset encoding extraction that avoids Axiom dependencies.
      * Used in JSON-only mode to prevent loading XML-oriented libraries.
diff --git 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPWorker.java
 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPWorker.java
index 8eacd44c46..9358521a19 100644
--- 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPWorker.java
+++ 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPWorker.java
@@ -366,12 +366,7 @@ public class HTTPWorker implements Worker {
      * @return true - if service metadata can be exposed, false - otherwise
      */
     private boolean canExposeServiceMetadata(AxisService service) throws 
IOException {
-        Parameter exposeServiceMetadata = 
service.getParameter("exposeServiceMetadata");
-        if (exposeServiceMetadata != null &&
-            JavaUtils.isFalseExplicitly(exposeServiceMetadata.getValue())) {
-            return false;
-        }
-        return true;
+        return service.isMetadataExposed();
     }
 
     private boolean processInternalWSDL(String uri, ConfigurationContext 
configurationContext, 
diff --git 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java
 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java
index da283c2d8b..0a0cea3a7d 100644
--- 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java
+++ 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java
@@ -349,12 +349,7 @@ public class ListingAgent extends AbstractAgent {
      * @return true - if service metadata can be exposed, false - otherwise
      */
     private boolean canExposeServiceMetadata(AxisService service) {
-        Parameter exposeServiceMetadata = 
service.getParameter("exposeServiceMetadata");
-        if(exposeServiceMetadata != null &&
-           JavaUtils.isFalseExplicitly(exposeServiceMetadata.getValue())) {
-           return false;
-        }
-        return true;
+        return service.isMetadataExposed();
     }
 
     protected void processListServices(HttpServletRequest req,
@@ -371,7 +366,14 @@ public class ListingAgent extends AbstractAgent {
         try {
             java.util.Map<String, AxisService> services = 
configContext.getAxisConfiguration().getServices();
             if (services != null) {
-                sortedServices.putAll(services);
+                // A service hidden from the metadata routes must not be named 
here
+                // either: the listing gives its name, EPR and every 
operation, which
+                // is what those routes were hidden to withhold.
+                for (java.util.Map.Entry<String, AxisService> entry : 
services.entrySet()) {
+                    if (entry.getValue() == null || 
entry.getValue().isMetadataExposed()) {
+                        sortedServices.put(entry.getKey(), entry.getValue());
+                    }
+                }
             }
         } catch (java.util.ConcurrentModificationException e) {
             // Hot deployment race — use whatever we captured so far
diff --git 
a/modules/transport/http/src/test/java/org/apache/axis2/transport/http/MetaInfResourceGuardTest.java
 
b/modules/transport/http/src/test/java/org/apache/axis2/transport/http/MetaInfResourceGuardTest.java
new file mode 100644
index 0000000000..74ba11c423
--- /dev/null
+++ 
b/modules/transport/http/src/test/java/org/apache/axis2/transport/http/MetaInfResourceGuardTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.transport.http;
+
+import junit.framework.TestCase;
+
+/**
+ * The {@code ?xsd=} route reaches a service's packaged META-INF with the 
request's
+ * value verbatim, and that directory holds more than schemas: {@code 
services.xml}
+ * names keystores and password-callback classes. Only schema and WSDL 
documents are
+ * servable, and the check lives inside the shared stream helper so that every 
caller
+ * inherits it.
+ */
+public class MetaInfResourceGuardTest extends TestCase {
+
+    public void testOnlySchemaAndWsdlDocumentsAreServable() {
+        assertTrue(HTTPTransportUtils.isServableMetadataResource("Foo.xsd"));
+        assertTrue(HTTPTransportUtils.isServableMetadataResource("Foo.wsdl"));
+        assertTrue("the extension check is case-insensitive",
+                HTTPTransportUtils.isServableMetadataResource("Foo.WSDL"));
+        assertTrue("a name inside a packaged subdirectory is still fine",
+                
HTTPTransportUtils.isServableMetadataResource("schemas/Foo.xsd"));
+    }
+
+    /** The finding: services.xml carries the deployment's security 
parameters. */
+    public void testServiceDescriptorsAreNotServable() {
+        
assertFalse(HTTPTransportUtils.isServableMetadataResource("services.xml"));
+        
assertFalse(HTTPTransportUtils.isServableMetadataResource("MANIFEST.MF"));
+        
assertFalse(HTTPTransportUtils.isServableMetadataResource("module.xml"));
+    }
+
+    public void testEscapesAndAbsoluteNamesAreNotServable() {
+        
assertFalse(HTTPTransportUtils.isServableMetadataResource("../../services.xml"));
+        assertFalse("an escape ending in .xsd is still an escape",
+                
HTTPTransportUtils.isServableMetadataResource("../../../etc/passwd.xsd"));
+        
assertFalse(HTTPTransportUtils.isServableMetadataResource("/etc/passwd.xsd"));
+        assertFalse("a URL would leave the archive entirely",
+                
HTTPTransportUtils.isServableMetadataResource("http://attacker.example.com/x.xsd";));
+    }
+
+    public void testEmptyAndNullNamesAreNotServable() {
+        assertFalse(HTTPTransportUtils.isServableMetadataResource(null));
+        assertFalse(HTTPTransportUtils.isServableMetadataResource(""));
+        assertFalse(HTTPTransportUtils.isServableMetadataResource(".xsd"));
+    }
+}
diff --git a/src/site/markdown/release-notes/2.0.2.md 
b/src/site/markdown/release-notes/2.0.2.md
index 43d1a28f0a..881a37b2c5 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -66,9 +66,18 @@ in `SECURITY.md`.
 - **`exposeServiceMetadata` is honoured uniformly.** Every anonymous metadata 
route
   now respects it: the `?wsdl`, `?wsdl2` and `?xsd` queries as before, plus the
   `.xsd`/`.wsdl` file routes on both the servlet and standalone HTTP paths, the
-  named-WSDL route, and the OpenAPI/Swagger/MCP generators. A service with 
exposure
-  disabled is skipped rather than refused, so it stays indistinguishable from 
one
-  that is not deployed.
+  named-WSDL route, the OpenAPI/Swagger/MCP generators, the `/services/` 
listing on
+  both render paths, WS-MEX `GetMetadata`, and the ping module's service-level 
ping.
+  A service with exposure disabled is skipped rather than refused, so it stays
+  indistinguishable from one that is not deployed. If you relied on a hidden 
service
+  still appearing in the listing, in a WS-MEX response or in a ping response, 
it no
+  longer will; set `exposeServiceMetadata` to true on that service.
+
+- **The `?xsd=` route serves only schema and WSDL documents.** It reached a 
service's
+  packaged META-INF with the request's value, so `?xsd=services.xml` returned 
the
+  service descriptor -- which names keystores and password-callback classes. 
Only
+  `.xsd` and `.wsdl` names that stay inside META-INF are served now, enforced 
inside
+  the shared helper so all three callers inherit it.
 
 - **OpenAPI and Swagger UI output.** Request-controlled values are validated 
and
   encoded for the context they are written into, the served page carries a

Reply via email to