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.
