No Fixed Address: Log4Shell-Style JNDI Injection in Apache Axis 1.x

Apache Axis 1.x is a SOAP engine whose last release came out in 2006. Twenty years later deps.dev counts 384 packages that still depend on 1.4, 83 of them directly, and that single version carries five published advisories. The worst of them turns the client-side service factory into a remote code execution primitive (tracked as CVE-2023-40743, NVD CVSS 9.8, critical). It is the Log4Shell bug class: an attacker-controlled string reaches a JNDI lookup, here through a service-factory parameter instead of a log message.
The reflex at this point is to open the advisory and look for the fixed version. There is no fixed version. The affected range for both Maven coordinates ends at 1.4 with nothing after it, because Apache does not expect to publish an Axis 1.x release that corrects the problem.
How does this affect you?
Nobody adds Axis 1.x to a pom.xml on purpose in 2026. It arrives underneath something else: a vendor's SOAP client, an integration adapter, a WSDL toolchain that someone generated stubs with a decade ago and never revisited. Most of those 384 dependents reach it transitively, which is why the package tends to surface in a scan report long after the team that introduced it has moved on.
Having 1.4 on the classpath makes an application vulnerable. It becomes exploitable when input you do not control reaches ServiceFactory.getService, which is more common than it sounds in code that resolves a service name from configuration, a request parameter, or a database row that an operator can edit.
Stubs from a WSDL
Axis 1.x is a SOAP engine. You point WSDL2Java at a WSDL file, it generates client stubs from the service definition, and your code calls the remote service through them as if it were a local object. Much of that generated code is still in production.
ServiceFactory is the JAX-RPC entry point that returns the Service object those stubs hang off. Its convenience is that it will reuse an instance the container already has bound in JNDI rather than constructing a fresh one on every call. You pass it a Map of environment settings, and it reads the JNDI name to look up out of that map, under the key jndiName.
The name game
Here is the relevant part of ServiceFactory.getService as it stands in 1.4:
if (context != null) {
String name = (String)environment.get("jndiName");
if (name == null) {
name = "axisServiceName";
}
// We've got JNDI, so try to find an AxisClient at the
// specified name.
try {
service = (Service)context.lookup(name);
} catch (NamingException e) {Any string under jndiName goes into context.lookup() unchecked.
In normal use that is unremarkable. JNDI is Java's naming layer, and a lookup is how you ask the container for something it holds under a label: jdbc/OrdersDB gets you a datasource, java:comp/env/greeting gets you a configured string. Names in, objects out.
The catch is that a name can also be a URL, and the URL scheme decides which provider resolves it. ldap://attacker.example/payload is a perfectly valid thing to hand InitialContext.lookup(). The JVM contacts that server and gets a reference back describing an object. On older JDKs it would then fetch and instantiate the class that reference names from the attacker's server, and the attacker is running code in your process. Current JDKs turn off that remote codebase loading for LDAP and RMI by default (trustURLCodebase=false), so on a modern JVM code execution usually needs a gadget or object factory already on the classpath. The outbound connection still happens regardless, which is why the CVE lists denial of service and SSRF alongside RCE. All of it starts with controlling a string in a Map, the same lookup-to-remote-class-loading path behind Log4Shell.
The fix took three commits
Upstream did write a fix, in three commits over eighteen months. None of them ever became something you can install, because there is no 1.4.1 to point a build at. Debian did patch this one in its own axis package (DLA-3622-1, October 2023, with fixed versions in later Debian releases too), but that fix lives in Debian's repositories, not on Maven Central. For a Maven build, following the fix meant watching a dead project's main branch with nothing to pin a build to.
The first commit, 7e66753 from August 2023, put a guard in front of the lookup that rejects the dangerous schemes by substring match. That is the commit the advisory points at, and the only one it names.
It was not the whole fix. Two more commits followed on the same file. Below is the shape of the guard after each, with the repeated name.toUpperCase().indexOf("X") != -1 calls abbreviated to has("X") so the structure is readable:
// after 7e66753 (Aug 2023)
if (name != null && (has("LDAP") || has("RMI") || has("JMS") || has("JMX"))
|| has("JRMP") || has("JAVA") || has("DNS")) {
return null;
}
// after 685c309 + 2c0d660 (Dec 2023 + Feb 2025)
if (name != null && (has("LDAP") || has("RMI") || has("JMS") || has("JMX")
|| has("JRMP") || has("JAVA") || has("DNS")
|| has("IIOP") || has("CORBANAME"))) {
return null;
}Commit 685c309, from December 2023, added IIOP and CORBANAME. Both are JNDI providers that do remote object resolution, and until that commit a jndiName of iiop:// or corbaname:// reached context.lookup() untouched. If you are still carrying CORBA naming in a running system you have probably earned the right to be tired, but the JVM will resolve it just the same, and a guard that blocks LDAP and RMI while leaving it open is a partial guard.
Commit 2c0d660, from February 2025, moved a closing parenthesis (that was the entire commit). Look at where the bracket sits in the first block: it closes after JMX, leaving JRMP, JAVA and DNS outside the name != null test. Java evaluates && before ||, so a call with no jndiName at all reached name.toUpperCase() on a null reference and threw a NullPointerException, where unpatched 1.4 would have quietly fallen back to axisServiceName. One character in the wrong place, and a safe default becomes a crash.
A team that did the diligent thing in late 2023, found the commit the advisory named and applied it to their own build, ended up with a filter that still let two remote-resolution schemes through and broke the default path on the way.
Nothing to upgrade to
The advisory offers three ways out, and not one of them is a fixed release. You can apply commit 7e66753 to your own build, which is the option that leaves iiop:// and corbaname:// open. You can migrate to Apache Axis 2/Java, a separate project whose API means rewriting every generated stub and every call site rather than bumping a version. Or you can review your own code so nothing untrusted reaches getService, a workaround that narrows who can reach the flaw without touching the flaw itself.
On the option everyone actually wants, Apache was candid:
The Apache Axis project does not expect to create an Axis 1.x release fixing this problem, though contributors that would like to work towards this are welcome.
So there is no release to move to, and the dependency graph does not care. When a vendor SOAP adapter three levels down your tree pins axis:axis:1.4, you cannot upgrade past the problem, and rewriting somebody else's adapter onto Axis 2 is not a security task you are scheduling this quarter.
Patching a package nobody is publishing
This is the case patch-in-place exists for. Seal Security backports the fix into the version your project already depends on and releases it as a drop-in replacement for that exact version, so nothing above it in your dependency tree has to move and the vulnerable code path is corrected underneath.
For this one that meant carrying all three upstream commits rather than the single one the advisory cites. The result was then checked on the built JAR against a stub JNDI provider, with no network involved: ldap, rmi, jrmp, iiop and corbaname names are rejected, a plain service name is allowed through, and a call with no jndiName falls back to looking up axisServiceName rather than throwing a NullPointerException.





