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
