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,
