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
