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 1b6856b4d7903780786c79327bed1b53ea051a15 Author: Robert Lazarski <[email protected]> AuthorDate: Wed Aug 5 07:08:56 2026 -1000 Refuse decoupled WS-Addressing responses by default Follows Apache CXF, whose implementation of the same decision is now visible in full: org.apache.cxf.ws.addressing.decoupled.enabled defaults to false and gates non-anonymous wsa:ReplyTo/FaultTo outright, with a scheme allowlist enforced even for pre-approved exchanges. Notably CXF implements no IP-range check at all -- the on/off gate plus the scheme list is their whole answer. That is the better primary control and it is now the Axis2 default too. The range check alone was a partial defence: refusing RFC-1918 and loopback still left the server able to deliver a response body to any public host the caller named, which is the exfiltration half of a non-blind SSRF. Refusing decoupled responses outright closes both halves, is a single boolean to document, and fails loudly so a deployment that needs the feature finds out immediately. blockPrivateNetworkResponseEndpoints stays as a secondary control for deployments that turn decoupled responses back on. CXF carved out WS-RM via a pre-approval property. Axis2 needs no equivalent: Sandesha is dead and the only in-tree references to it are commented out. The feature is exercised by 17 integration tests -- the async, two-channel, third-party-callback and separate-listener MTOM cases -- which now opt in through their test repositories. That opt-in is the migration a real deployment makes, so the tests double as its documentation. Such deployments should also set httpFrontendHostUrl, since the generated reply address otherwise defaults to the client machine's local IP. Co-Authored-By: Claude Fable 5 <[email protected]> --- .../addressing/ResponseEndpointPolicy.java | 14 ++++---- .../addressing/AddressingFinalInHandlerTest.java | 6 ++++ .../ResponseEndpointPolicyHandlerTest.java | 26 ++++++++++++++ .../addressing/ResponseEndpointPolicyTest.java | 40 +++++++++++++++------ modules/integration/pom.xml | 42 ++++++++++++++++++++++ .../test-resources/mtom/MTOM-enabled-axis2.xml | 4 +++ .../mtom/MTOM-fileCache-enabled-axis2.xml | 4 +++ modules/kernel/conf/axis2.xml | 17 ++++++--- modules/webapp/conf/axis2.xml | 17 ++++++--- 9 files changed, 145 insertions(+), 25 deletions(-) diff --git a/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java b/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java index f9d7a3fe28..83fcfa058d 100644 --- a/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java +++ b/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java @@ -63,11 +63,13 @@ import java.util.concurrent.TimeoutException; * (service, service group, then {@code axis2.xml}): * * <dl> - * <dt>{@code allowNonAnonymousResponseEndpoints} (default {@code true})</dt> - * <dd>Set to {@code false} to refuse every non-anonymous response endpoint, so - * replies and faults only ever travel back down the inbound connection. This is - * the strictest posture and the right one for a deployment that does not use - * decoupled or dual-channel responses.</dd> + * <dt>{@code allowNonAnonymousResponseEndpoints} (default {@code false})</dt> + * <dd>Off by default, so replies and faults only ever travel back down the + * inbound connection and an inbound header cannot name an outbound destination + * at all. Apache CXF made the same call for the same reason in its + * {@code org.apache.cxf.ws.addressing.decoupled.enabled} property. Set it to + * {@code true} for a deployment that genuinely uses decoupled responses — the + * separate-listener ("Dual") clients, or a third-party callback endpoint.</dd> * * <dt>{@code blockPrivateNetworkResponseEndpoints} (default {@code false})</dt> * <dd>Additionally rejects response endpoints that resolve to loopback, @@ -143,7 +145,7 @@ final class ResponseEndpointPolicy { return true; } - if (!booleanParameter(messageContext, ALLOW_NON_ANONYMOUS, true)) { + if (!booleanParameter(messageContext, ALLOW_NON_ANONYMOUS, false)) { log.warn("Rejecting non-anonymous WS-Addressing response endpoint: " + ALLOW_NON_ANONYMOUS + " is false"); return false; diff --git a/modules/addressing/test/org/apache/axis2/handlers/addressing/AddressingFinalInHandlerTest.java b/modules/addressing/test/org/apache/axis2/handlers/addressing/AddressingFinalInHandlerTest.java index 1918f246f2..0015569bc9 100644 --- a/modules/addressing/test/org/apache/axis2/handlers/addressing/AddressingFinalInHandlerTest.java +++ b/modules/addressing/test/org/apache/axis2/handlers/addressing/AddressingFinalInHandlerTest.java @@ -217,6 +217,12 @@ public class AddressingFinalInHandlerTest extends AddressingInHandlerTestBase { MessageContext mc = new MessageContext(); mc.setConfigurationContext(ConfigurationContextFactory.createEmptyConfigurationContext()); mc.setServerSide(true); + // This fixture carries a non-anonymous wsa:ReplyTo and the test is about + // SOAPAction matching, so opt in to decoupled responses rather than have + // the egress policy fault the message first. + mc.getConfigurationContext().getAxisConfiguration().addParameter( + new org.apache.axis2.description.Parameter( + "allowNonAnonymousResponseEndpoints", "true")); try { mc.setSoapAction("http://ws.apache.org/tests/action"); basicExtractAddressingInformationFromHeaders(testfile, mc); diff --git a/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyHandlerTest.java b/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyHandlerTest.java index 82273ccaaf..970294cf6b 100644 --- a/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyHandlerTest.java +++ b/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyHandlerTest.java @@ -51,6 +51,32 @@ public class ResponseEndpointPolicyHandlerTest extends TestCase { super.setUp(); handler = new AddressingInHandler(); configurationContext = ConfigurationContextFactory.createEmptyConfigurationContext(); + // Decoupled responses are off by default; opt in so these tests reach the + // address checks. testDecoupledReplyToRefusedByDefault covers the default. + configurationContext.getAxisConfiguration().addParameter( + new Parameter(ResponseEndpointPolicy.ALLOW_NON_ANONYMOUS, "true")); + } + + /** + * Out of the box a non-anonymous ReplyTo is refused outright, whatever it + * points at, so an inbound header cannot name an outbound destination. + */ + public void testDecoupledReplyToRefusedByDefault() throws Exception { + configurationContext = ConfigurationContextFactory.createEmptyConfigurationContext(); + try { + invokeWith("ReplyTo", "http://192.0.2.25/replies"); + fail("A non-anonymous ReplyTo should fault under the shipped default"); + } catch (AxisFault expected) { + // expected + } + } + + /** Anonymous replies are unaffected, or ordinary in-out messaging breaks. */ + public void testAnonymousReplyToStillWorksByDefault() throws Exception { + configurationContext = ConfigurationContextFactory.createEmptyConfigurationContext(); + MessageContext mc = invokeWith("ReplyTo", + org.apache.axis2.addressing.AddressingConstants.Final.WSA_ANONYMOUS_URL); + assertTrue(mc.getReplyTo().hasAnonymousAddress()); } /** diff --git a/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyTest.java b/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyTest.java index 0d1af45aa8..478d0acdf8 100644 --- a/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyTest.java +++ b/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyTest.java @@ -42,6 +42,18 @@ public class ResponseEndpointPolicyTest extends TestCase { ConfigurationContext configurationContext = new ConfigurationContext(axisConfiguration); messageContext = configurationContext.createMessageContext(); messageContext.setServerSide(true); + // Decoupled responses are off by default. Most tests here cover what the + // policy does to an endpoint it is actually willing to consider, so they + // opt in; testDecoupledResponsesAreOffByDefault covers the default. + setParameter(ResponseEndpointPolicy.ALLOW_NON_ANONYMOUS, "true"); + } + + /** A context without the opt-in, for testing the shipped default. */ + private MessageContext defaultConfigured() throws Exception { + AxisConfiguration config = new AxisConfiguration(); + MessageContext mc = new ConfigurationContext(config).createMessageContext(); + mc.setServerSide(true); + return mc; } private void setParameter(String name, String value) throws Exception { @@ -75,7 +87,7 @@ public class ResponseEndpointPolicyTest extends TestCase { * ThirdPartyResponseRawXMLTest replies to 127.0.0.1 — so the default policy * must not refuse them. */ - public void testLoopbackAndPrivateAddressesAreAllowedByDefault() { + public void testLoopbackAndPrivateAddressesAllowedWhenDecoupledEnabled() { assertTrue(ResponseEndpointPolicy.isAllowed( new EndpointReference("http://127.0.0.1:8080/sink"), messageContext)); assertTrue(ResponseEndpointPolicy.isAllowed( @@ -161,23 +173,29 @@ public class ResponseEndpointPolicyTest extends TestCase { * A routable public address is still permitted by default, so decoupled * responses to a genuine external endpoint keep working. */ - public void testPublicAddressIsAllowedByDefault() { + public void testPublicAddressAllowedWhenDecoupledEnabled() { EndpointReference publicEpr = new EndpointReference("http://192.0.2.25/replies"); assertTrue(ResponseEndpointPolicy.isAllowed(publicEpr, messageContext)); } /** - * The strict posture — the equivalent of what CXF made its default — refuses - * every non-anonymous response endpoint. + * The shipped default refuses every non-anonymous response endpoint, so no + * inbound header can name an outbound destination at all — the same call + * Apache CXF made in org.apache.cxf.ws.addressing.decoupled.enabled. */ - public void testNonAnonymousCanBeDisabledEntirely() throws Exception { - setParameter(ResponseEndpointPolicy.ALLOW_NON_ANONYMOUS, "false"); - assertFalse(ResponseEndpointPolicy.isAllowed( - new EndpointReference("http://192.0.2.25/replies"), messageContext)); - // The anonymous case must still work, or in-out messaging breaks. + public void testDecoupledResponsesAreOffByDefault() throws Exception { + MessageContext defaults = defaultConfigured(); + assertFalse("A public reply endpoint must be refused by default", + ResponseEndpointPolicy.isAllowed( + new EndpointReference("http://192.0.2.25/replies"), defaults)); + assertFalse("A private reply endpoint must be refused by default", + ResponseEndpointPolicy.isAllowed( + new EndpointReference("http://10.1.2.3/internal"), defaults)); + + // Anonymous must still work by default, or ordinary in-out messaging breaks. assertTrue(ResponseEndpointPolicy.isAllowed( - new EndpointReference(AddressingConstants.Final.WSA_ANONYMOUS_URL), - messageContext)); + new EndpointReference(AddressingConstants.Final.WSA_ANONYMOUS_URL), defaults)); + assertTrue(ResponseEndpointPolicy.isAllowed(null, defaults)); } /** diff --git a/modules/integration/pom.xml b/modules/integration/pom.xml index 6887febc14..2ad66dba7b 100644 --- a/modules/integration/pom.xml +++ b/modules/integration/pom.xml @@ -254,6 +254,20 @@ <modules> <module>addressing</module> </modules> + <!-- + Decoupled WS-Addressing responses are off by default + (allowNonAnonymousResponseEndpoints in axis2.xml). The + async, two-channel and third-party-callback tests here + exercise exactly that feature, so they opt in - which + also demonstrates the migration a deployment using + those clients has to make. + --> + <parameters> + <parameter> + <name>allowNonAnonymousResponseEndpoints</name> + <value>true</value> + </parameter> + </parameters> <handlers> <handler> <flow>InFlow</flow> @@ -278,6 +292,20 @@ <modules> <module>addressing</module> </modules> + <!-- + Decoupled WS-Addressing responses are off by default + (allowNonAnonymousResponseEndpoints in axis2.xml). The + async, two-channel and third-party-callback tests here + exercise exactly that feature, so they opt in - which + also demonstrates the migration a deployment using + those clients has to make. + --> + <parameters> + <parameter> + <name>allowNonAnonymousResponseEndpoints</name> + <value>true</value> + </parameter> + </parameters> </generatedAxis2xml> <modules>addressing</modules> </configuration> @@ -329,6 +357,20 @@ <modules> <module>addressing</module> </modules> + <!-- + Decoupled WS-Addressing responses are off by default + (see allowNonAnonymousResponseEndpoints in axis2.xml). + The async, two-channel and third-party-callback tests + in this module exercise exactly that feature, so they + opt in here - which also demonstrates the migration a + deployment using those clients has to make. + --> + <parameters> + <parameter> + <name>allowNonAnonymousResponseEndpoints</name> + <value>true</value> + </parameter> + </parameters> </generatedAxis2xml> <modules>addressing</modules> </configuration> diff --git a/modules/integration/test-resources/mtom/MTOM-enabled-axis2.xml b/modules/integration/test-resources/mtom/MTOM-enabled-axis2.xml index 78c25ab096..2c55ca73d5 100644 --- a/modules/integration/test-resources/mtom/MTOM-enabled-axis2.xml +++ b/modules/integration/test-resources/mtom/MTOM-enabled-axis2.xml @@ -18,6 +18,10 @@ --> <axisconfig name="AxisJava2.0"> + <!-- The separate-listener MTOM tests use a decoupled wsa:ReplyTo, which is + off by default; opt in so they exercise that path. --> + <parameter name="allowNonAnonymousResponseEndpoints">true</parameter> + <!-- ================================================= --> <!-- Parameters --> <!-- ================================================= --> diff --git a/modules/integration/test-resources/mtom/MTOM-fileCache-enabled-axis2.xml b/modules/integration/test-resources/mtom/MTOM-fileCache-enabled-axis2.xml index 45c701294f..0a86f27802 100644 --- a/modules/integration/test-resources/mtom/MTOM-fileCache-enabled-axis2.xml +++ b/modules/integration/test-resources/mtom/MTOM-fileCache-enabled-axis2.xml @@ -18,6 +18,10 @@ --> <axisconfig name="AxisJava2.0"> + <!-- The separate-listener MTOM tests use a decoupled wsa:ReplyTo, which is + off by default; opt in so they exercise that path. --> + <parameter name="allowNonAnonymousResponseEndpoints">true</parameter> + <!-- ================================================= --> <!-- Parameters --> <!-- ================================================= --> diff --git a/modules/kernel/conf/axis2.xml b/modules/kernel/conf/axis2.xml index 1c474c016e..8bc34b9091 100644 --- a/modules/kernel/conf/axis2.xml +++ b/modules/kernel/conf/axis2.xml @@ -72,9 +72,18 @@ are wired. Turn it on wherever the caller is not already inside the trusted network. - Set allowNonAnonymousResponseEndpoints to false to refuse decoupled - responses entirely - the right setting unless this deployment actually uses - dual-channel or decoupled WS-Addressing responses. + allowNonAnonymousResponseEndpoints is FALSE by default as of 2.0.2: a + reply or fault only ever travels back down the inbound connection, so an + inbound header cannot name an outbound destination at all. Apache CXF made + the same call for the same reason in its + org.apache.cxf.ws.addressing.decoupled.enabled property. + + Set it to true only if this deployment genuinely uses decoupled responses - + the separate-listener "Dual" clients (Options.setUseSeparateListener(true), + see samples/userguide EchoBlockingDualClient) or a third-party callback + endpoint. Those deployments should also set httpFrontendHostUrl so the + generated reply address is the real external URL rather than the client + machine's local IP. allowedResponseEndpointHosts, when set to a comma-separated host list, restricts response endpoints to exactly those hosts. @@ -88,7 +97,7 @@ list is not restricted to HTTP. Schemes outside it, such as file or gopher, are refused. Add to this list if a custom transport is in use. --> - <parameter name="allowNonAnonymousResponseEndpoints">true</parameter> + <parameter name="allowNonAnonymousResponseEndpoints">false</parameter> <parameter name="blockPrivateNetworkResponseEndpoints">false</parameter> <!--<parameter name="allowedResponseEndpointHosts">replies.example.com</parameter>--> <!--<parameter name="allowedResponseEndpointSchemes">http,https</parameter>--> diff --git a/modules/webapp/conf/axis2.xml b/modules/webapp/conf/axis2.xml index a26c7723fe..2e8aecbe9b 100644 --- a/modules/webapp/conf/axis2.xml +++ b/modules/webapp/conf/axis2.xml @@ -72,9 +72,18 @@ are wired. Turn it on wherever the caller is not already inside the trusted network. - Set allowNonAnonymousResponseEndpoints to false to refuse decoupled - responses entirely - the right setting unless this deployment actually uses - dual-channel or decoupled WS-Addressing responses. + allowNonAnonymousResponseEndpoints is FALSE by default as of 2.0.2: a + reply or fault only ever travels back down the inbound connection, so an + inbound header cannot name an outbound destination at all. Apache CXF made + the same call for the same reason in its + org.apache.cxf.ws.addressing.decoupled.enabled property. + + Set it to true only if this deployment genuinely uses decoupled responses - + the separate-listener "Dual" clients (Options.setUseSeparateListener(true), + see samples/userguide EchoBlockingDualClient) or a third-party callback + endpoint. Those deployments should also set httpFrontendHostUrl so the + generated reply address is the real external URL rather than the client + machine's local IP. allowedResponseEndpointHosts, when set to a comma-separated host list, restricts response endpoints to exactly those hosts. @@ -88,7 +97,7 @@ list is not restricted to HTTP. Schemes outside it, such as file or gopher, are refused. Add to this list if a custom transport is in use. --> - <parameter name="allowNonAnonymousResponseEndpoints">true</parameter> + <parameter name="allowNonAnonymousResponseEndpoints">false</parameter> <parameter name="blockPrivateNetworkResponseEndpoints">false</parameter> <!--<parameter name="allowedResponseEndpointHosts">replies.example.com</parameter>--> <!--<parameter name="allowedResponseEndpointSchemes">http,https</parameter>-->
