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 1c092af17f5fe7529f3d97d8cce6a7a58025b11b
Author: Robert Lazarski <[email protected]>
AuthorDate: Thu Sep 3 04:08:41 2026 -1000

    AXIS2-6062: screen JMS addresses by JNDI scheme, not by substring
    
    The guard searched the whole target address for LDAP, RMI, JMX, JRMP, DNS, 
IIOP
    and CORBANAME, so a queue named alarming was refused for containing "RMI" 
while
    a hostile provider URL naming none of those words passed. The schemes 
matter in
    the scheme position of the three fields JNDI actually resolves: the 
destination
    name, the reply destination and the provider URL. Screen those instead; 
every
    address the old guard refused for a real reason is still refused.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../org/apache/axis2/transport/jms/JMSSender.java  |  12 +-
 .../transport/jms/JMSTargetAddressPolicy.java      | 122 +++++++++++++++++++++
 .../transport/jms/JMSTargetAddressPolicyTest.java  |  86 +++++++++++++++
 src/site/markdown/release-notes/2.0.2.md           |   9 ++
 4 files changed, 226 insertions(+), 3 deletions(-)

diff --git 
a/modules/transport/jms/src/main/java/org/apache/axis2/transport/jms/JMSSender.java
 
b/modules/transport/jms/src/main/java/org/apache/axis2/transport/jms/JMSSender.java
index 8dfad332a7..8fad3660cc 100644
--- 
a/modules/transport/jms/src/main/java/org/apache/axis2/transport/jms/JMSSender.java
+++ 
b/modules/transport/jms/src/main/java/org/apache/axis2/transport/jms/JMSSender.java
@@ -122,9 +122,15 @@ public class JMSSender extends AbstractTransportSender 
implements ManagementSupp
         JMSOutTransportInfo jmsOut = null;
         JMSMessageSender messageSender = null;
 
-        if (targetAddress != null && 
(targetAddress.toUpperCase().indexOf("LDAP")!=-1 || 
targetAddress.toUpperCase().indexOf("RMI")!=-1 || 
targetAddress.toUpperCase().indexOf("JMX")!=-1 || 
targetAddress.toUpperCase().indexOf("JRMP")!=-1 || 
targetAddress.toUpperCase().indexOf("DNS")!=-1 || 
targetAddress.toUpperCase().indexOf("IIOP")!=-1 || 
targetAddress.toUpperCase().indexOf("CORBANAME")!=-1)) {
-            throw new AxisFault("targetAddress received by JMSSender is not 
supported by this method: " + targetAddress);
-       }
+        // AXIS2-6062: a JMS address becomes JNDI lookups, and a name carrying 
a
+        // remote naming scheme resolves through that scheme's URL context 
factory.
+        // See JMSTargetAddressPolicy for why this screens the scheme position 
of the
+        // resolved fields rather than searching the whole address for 
substrings.
+        String rejection = 
JMSTargetAddressPolicy.rejectionReason(targetAddress);
+        if (rejection != null) {
+            handleException("Refusing the JMS target address because " + 
rejection
+                    + ": " + targetAddress);
+        }
 
         // A decoupled response goes to a destination the caller named, so 
this EPR's
         // query string is attacker-supplied. JMSOutTransportInfo hands that 
query
diff --git 
a/modules/transport/jms/src/main/java/org/apache/axis2/transport/jms/JMSTargetAddressPolicy.java
 
b/modules/transport/jms/src/main/java/org/apache/axis2/transport/jms/JMSTargetAddressPolicy.java
new file mode 100644
index 0000000000..89bcf84e0d
--- /dev/null
+++ 
b/modules/transport/jms/src/main/java/org/apache/axis2/transport/jms/JMSTargetAddressPolicy.java
@@ -0,0 +1,122 @@
+/*
+ * 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.jms;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.naming.Context;
+
+import org.apache.axis2.transport.base.BaseUtils;
+
+/**
+ * Screens a JMS target address for JNDI names that resolve somewhere remote.
+ * <p>
+ * A JMS EPR is turned into two JNDI lookups and one JNDI environment:
+ * <ul>
+ * <li>the destination name, which is everything between {@code jms:/} and the 
query
+ *     string ({@link JMSUtils#getDestination}),</li>
+ * <li>the reply destination named by {@code 
transport.jms.ReplyDestination},</li>
+ * <li>{@code java.naming.provider.url}, which says where names resolve.</li>
+ * </ul>
+ * JNDI resolves a name carrying a URL scheme through that scheme's URL context
+ * factory, and several of those factories fetch and deserialise a remote 
object --
+ * the JNDI injection route to code execution. So {@code jms:/ldap://host/obj} 
is a
+ * lookup against an attacker's directory, whatever the configured provider is.
+ * <p>
+ * This replaces an earlier guard (AXIS2-6062) that searched the whole address 
for
+ * the substrings LDAP, RMI, JMX, JRMP, DNS, IIOP and CORBANAME. That caught 
the
+ * shapes above, but it also refused any legitimate destination whose name 
happened
+ * to contain one -- a queue called {@code alarming} contains "RMI" -- while 
missing
+ * a hostile provider URL naming no such scheme. Matching the scheme position 
of the
+ * fields that are actually resolved is both stricter where it matters and 
free of
+ * those false positives. Do not replace it with a substring search again.
+ */
+final class JMSTargetAddressPolicy {
+
+    /**
+     * Schemes whose JNDI URL context factories reach out to a remote naming 
service
+     * and can return a deserialised or remotely-loaded object.
+     */
+    private static final Set<String> REMOTE_JNDI_SCHEMES = 
Collections.unmodifiableSet(
+            new HashSet<String>(Arrays.asList(
+                    "ldap", "ldaps", "rmi", "iiop", "iiopname",
+                    "corbaname", "corbaloc", "jrmp", "dns", "jmx")));
+
+    /** A leading URL scheme, per RFC 3986. */
+    private static final Pattern SCHEME = 
Pattern.compile("^([A-Za-z][A-Za-z0-9+.\\-]*):");
+
+    private JMSTargetAddressPolicy() {
+    }
+
+    /**
+     * @param targetAddress the JMS EPR about to be resolved
+     * @return why the address must be refused, or {@code null} if it is 
acceptable
+     */
+    static String rejectionReason(String targetAddress) {
+        if (targetAddress == null || 
!targetAddress.startsWith(JMSConstants.JMS_PREFIX)) {
+            return null;
+        }
+
+        String reason = checkLookupName("destination name",
+                JMSUtils.getDestination(targetAddress));
+        if (reason != null) {
+            return reason;
+        }
+
+        Map<String, String> properties = 
BaseUtils.getEPRProperties(targetAddress);
+        reason = checkLookupName("reply destination name",
+                properties.get(JMSConstants.PARAM_REPLY_DESTINATION));
+        if (reason != null) {
+            return reason;
+        }
+
+        String providerUrl = properties.get(Context.PROVIDER_URL);
+        String providerScheme = schemeOf(providerUrl);
+        if (providerScheme != null && 
REMOTE_JNDI_SCHEMES.contains(providerScheme)) {
+            return "the JNDI provider URL uses the remote naming scheme '"
+                    + providerScheme + "'";
+        }
+
+        return null;
+    }
+
+    private static String checkLookupName(String what, String name) {
+        String scheme = schemeOf(name);
+        if (scheme != null && REMOTE_JNDI_SCHEMES.contains(scheme)) {
+            return "the " + what + " is a '" + scheme + "' URL, which JNDI 
would "
+                    + "resolve through a remote naming service";
+        }
+        return null;
+    }
+
+    private static String schemeOf(String value) {
+        if (value == null) {
+            return null;
+        }
+        Matcher matcher = SCHEME.matcher(value.trim());
+        return matcher.find() ? matcher.group(1).toLowerCase(Locale.ENGLISH) : 
null;
+    }
+}
diff --git 
a/modules/transport/jms/src/test/java/org/apache/axis2/transport/jms/JMSTargetAddressPolicyTest.java
 
b/modules/transport/jms/src/test/java/org/apache/axis2/transport/jms/JMSTargetAddressPolicyTest.java
new file mode 100644
index 0000000000..3ec3869ae2
--- /dev/null
+++ 
b/modules/transport/jms/src/test/java/org/apache/axis2/transport/jms/JMSTargetAddressPolicyTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.jms;
+
+import junit.framework.TestCase;
+
+/**
+ * A JMS address becomes JNDI lookups, and JNDI resolves a name carrying a 
remote
+ * naming scheme through that scheme's URL context factory, which can fetch and
+ * deserialise a remote object. These tests pin which addresses are refused, 
and --
+ * just as importantly -- which are not: the guard this replaced (AXIS2-6062) 
searched
+ * the whole address for substrings and refused ordinary destination names that
+ * happened to contain one.
+ */
+public class JMSTargetAddressPolicyTest extends TestCase {
+
+    private void assertRefused(String address) {
+        String reason = JMSTargetAddressPolicy.rejectionReason(address);
+        assertNotNull("must be refused: " + address, reason);
+    }
+
+    private void assertAccepted(String address) {
+        String reason = JMSTargetAddressPolicy.rejectionReason(address);
+        assertNull("must be accepted: " + address + " (refused because " + 
reason + ")",
+                reason);
+    }
+
+    /** The destination name is a JNDI URL, so the lookup leaves the broker 
entirely. */
+    public void testRemoteSchemeAsDestinationNameIsRefused() {
+        assertRefused("jms:/ldap://attacker.example.com:1389/Exploit";);
+        assertRefused("jms:/rmi://attacker.example.com:1099/Exploit");
+        assertRefused("jms:/LDAP://attacker.example.com/Exploit");
+        assertRefused("jms:/iiopname://attacker.example.com/Exploit");
+        assertRefused("jms:/corbaname:iiop:attacker.example.com#x");
+        assertRefused("jms:/dns://attacker.example.com/x");
+    }
+
+    public void testRemoteSchemeAsReplyDestinationIsRefused() {
+        
assertRefused("jms:/Queue?transport.jms.ReplyDestination=ldap://attacker.example.com/x";);
+    }
+
+    /** The provider URL decides where every name resolves, including plain 
ones. */
+    public void testRemoteSchemeAsProviderUrlIsRefused() {
+        
assertRefused("jms:/Queue?java.naming.provider.url=ldap://attacker.example.com:1389";);
+        
assertRefused("jms:/Queue?java.naming.provider.url=rmi://attacker.example.com:1099");
+    }
+
+    /**
+     * The false positives the substring guard produced. A queue called 
"alarming"
+     * contains RMI; "dns-events" contains DNS. These are ordinary names and 
must work.
+     */
+    public void testOrdinaryNamesContainingSchemeSubstringsAreAccepted() {
+        assertAccepted("jms:/alarming");
+        assertAccepted("jms:/dns-events");
+        assertAccepted("jms:/PersonRMIQueue");
+        assertAccepted("jms:/jmx-metrics?transport.jms.DestinationType=topic");
+    }
+
+    public void testOrdinaryJmsAddressesAreAccepted() {
+        assertAccepted("jms:/ReplyQueue");
+        assertAccepted("jms:/java:comp/env/jms/MyQueue");
+        
assertAccepted("jms:/Queue?java.naming.provider.url=tcp://broker.internal:61616");
+        
assertAccepted("jms:/Queue?transport.jms.ReplyDestination=ResponseQueue");
+    }
+
+    public void testNonJmsAndNullAddressesAreLeftAlone() {
+        assertAccepted(null);
+        assertAccepted("http://example.com/service";);
+    }
+}
diff --git a/src/site/markdown/release-notes/2.0.2.md 
b/src/site/markdown/release-notes/2.0.2.md
index f70bffd089..43d1a28f0a 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -45,6 +45,15 @@ in `SECURITY.md`.
   connects to. Deployments replying through a particular provider name it in 
their
   own transport configuration.
 
+- **JMS target addresses are screened more precisely.** A JMS address becomes 
JNDI
+  lookups, and JNDI resolves a name carrying a remote naming scheme (`ldap:`, 
`rmi:`,
+  `iiop:`, `corbaname:`, `dns:` and friends) through that scheme's URL context
+  factory, which can fetch and deserialise a remote object. Those schemes are 
refused
+  in the destination name, the reply destination and the JNDI provider URL. 
This
+  replaces a guard that searched the whole address for those words as 
substrings and
+  so refused ordinary destinations whose names contained one: a queue called
+  `alarming` contains "RMI", `dns-events` contains "DNS". Such names now work 
again.
+
 - **Request bodies are bounded.** The `multipart/form-data` and
   `application/x-www-form-urlencoded` builders read the transport stream 
directly,
   so a servlet container's post-size limit never saw the body.

Reply via email to