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 fe5e5eebeebc71404182fbb570b53c4d33e4feb4 Author: Robert Lazarski <[email protected]> AuthorDate: Wed Sep 2 18:41:24 2026 -1000 Do not follow redirects on a decoupled response over HTTP/2 The HTTP sender stopped following them in 75191f7752, but the HTTP/2 sender never had the check and its async client follows redirects by default too, so a reply endpoint answering 307 could still send the server to an address the scheme and range checks had refused. The predicate both senders now share lives next to those checks, as AddressingResponseEndpointPolicy is what a redirect would otherwise walk past. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> --- .../AddressingResponseEndpointPolicy.java | 22 +++++++++++++ .../AddressingResponseEndpointPolicyTest.java | 37 ++++++++++++++++++++++ .../h2/impl/httpclient5/H2RequestImpl.java | 9 ++++++ .../http/impl/httpclient5/RequestImpl.java | 17 +++------- 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/modules/kernel/src/org/apache/axis2/addressing/AddressingResponseEndpointPolicy.java b/modules/kernel/src/org/apache/axis2/addressing/AddressingResponseEndpointPolicy.java index c9b10946d3..654dc16ff1 100644 --- a/modules/kernel/src/org/apache/axis2/addressing/AddressingResponseEndpointPolicy.java +++ b/modules/kernel/src/org/apache/axis2/addressing/AddressingResponseEndpointPolicy.java @@ -427,6 +427,28 @@ public final class AddressingResponseEndpointPolicy { } } + /** + * Whether this send is a WS-Addressing decoupled response: a server-side send to + * a destination the caller named, rather than an ordinary client-side request. + * <p> + * Transports use this to decide whether to follow redirects. The scheme allow-list + * and the address checks in {@link #isAllowed} run against the endpoint reference + * the caller supplied, and nothing re-examines where a redirect leads, so following + * one steps straight past them: a 307 from the caller's own endpoint to an address + * this policy had already refused would be honoured. A decoupled reply is + * fire-and-forget, so nothing legitimate depends on following a redirect. + * + * @param messageContext the message being sent; may be {@code null} + * @return whether the destination was nominated by the caller + */ + public static boolean isDecoupledResponse(MessageContext messageContext) { + if (messageContext == null || !messageContext.isServerSide()) { + return false; + } + EndpointReference to = messageContext.getTo(); + return to != null && !to.hasAnonymousAddress() && !to.hasNoneAddress(); + } + private static boolean booleanParameter(MessageContext messageContext, String name, boolean defaultValue) { if (messageContext == null) { diff --git a/modules/kernel/test/org/apache/axis2/addressing/AddressingResponseEndpointPolicyTest.java b/modules/kernel/test/org/apache/axis2/addressing/AddressingResponseEndpointPolicyTest.java index 8e3773c6ce..3d2a90e2a6 100644 --- a/modules/kernel/test/org/apache/axis2/addressing/AddressingResponseEndpointPolicyTest.java +++ b/modules/kernel/test/org/apache/axis2/addressing/AddressingResponseEndpointPolicyTest.java @@ -356,4 +356,41 @@ public class AddressingResponseEndpointPolicyTest extends TestCase { assertFalse(AddressingResponseEndpointPolicy.isAllowed( new EndpointReference("http:///no-host"), messageContext)); } + + /** + * The predicate transports use to decide whether to follow redirects. A redirect + * is only refused where the destination came from the caller, which is the + * decoupled-response case; ordinary client requests keep following them. + */ + public void testDecoupledResponseIsOnlyServerSideNonAnonymous() { + messageContext.setServerSide(true); + messageContext.setTo(new EndpointReference("https://caller.example.com/replies")); + assertTrue("a server-side send to a caller-named address is a decoupled response", + AddressingResponseEndpointPolicy.isDecoupledResponse(messageContext)); + } + + public void testClientRequestIsNotADecoupledResponse() { + messageContext.setServerSide(false); + messageContext.setTo(new EndpointReference("https://service.example.com/svc")); + assertFalse("a client-side request must keep following redirects", + AddressingResponseEndpointPolicy.isDecoupledResponse(messageContext)); + } + + public void testAnonymousAndNoneRepliesAreNotDecoupled() { + messageContext.setServerSide(true); + messageContext.setTo(new EndpointReference(AddressingConstants.Final.WSA_ANONYMOUS_URL)); + assertFalse("an anonymous reply goes back down the inbound connection", + AddressingResponseEndpointPolicy.isDecoupledResponse(messageContext)); + + messageContext.setTo(new EndpointReference(AddressingConstants.Final.WSA_NONE_URI)); + assertFalse("a none reply is not sent anywhere", + AddressingResponseEndpointPolicy.isDecoupledResponse(messageContext)); + } + + public void testMissingDestinationIsNotDecoupled() { + messageContext.setServerSide(true); + messageContext.setTo(null); + assertFalse(AddressingResponseEndpointPolicy.isDecoupledResponse(messageContext)); + assertFalse(AddressingResponseEndpointPolicy.isDecoupledResponse(null)); + } } diff --git a/modules/transport-h2/src/main/java/org/apache/axis2/transport/h2/impl/httpclient5/H2RequestImpl.java b/modules/transport-h2/src/main/java/org/apache/axis2/transport/h2/impl/httpclient5/H2RequestImpl.java index e75a34f591..2a3919511e 100644 --- a/modules/transport-h2/src/main/java/org/apache/axis2/transport/h2/impl/httpclient5/H2RequestImpl.java +++ b/modules/transport-h2/src/main/java/org/apache/axis2/transport/h2/impl/httpclient5/H2RequestImpl.java @@ -31,6 +31,7 @@ import java.util.concurrent.TimeUnit; import org.apache.axiom.mime.Header; import org.apache.axis2.AxisFault; +import org.apache.axis2.addressing.AddressingResponseEndpointPolicy; import org.apache.axis2.context.MessageContext; import org.apache.axis2.kernel.http.HTTPConstants; import org.apache.axis2.transport.http.AxisRequestEntity; @@ -278,6 +279,14 @@ public class H2RequestImpl implements Request { requestConfig.setCookieSpec(cookiePolicy); } + // Following a redirect on a decoupled response would step past the scheme + // and address checks, which only ever saw the caller-supplied destination; + // see AddressingResponseEndpointPolicy.isDecoupledResponse. The async client + // follows redirects by default just as the blocking one does. + if (AddressingResponseEndpointPolicy.isDecoupledResponse(msgContext)) { + requestConfig.setRedirectsEnabled(false); + } + clientContext.setRequestConfig(requestConfig.build()); // Remove Content-Length header to avoid ProtocolException (AXIS2-6051) diff --git a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/impl/httpclient5/RequestImpl.java b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/impl/httpclient5/RequestImpl.java index 5901857651..a2249b388c 100644 --- a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/impl/httpclient5/RequestImpl.java +++ b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/impl/httpclient5/RequestImpl.java @@ -28,7 +28,7 @@ import java.util.Map; import org.apache.axiom.mime.Header; import org.apache.axis2.AxisFault; -import org.apache.axis2.addressing.EndpointReference; +import org.apache.axis2.addressing.AddressingResponseEndpointPolicy; import org.apache.axis2.context.MessageContext; import org.apache.axis2.kernel.http.HTTPConstants; import org.apache.axis2.transport.http.AxisRequestEntity; @@ -237,17 +237,10 @@ final class RequestImpl implements Request { requestConfig.setCookieSpec(cookiePolicy); } - // A server-side send to a non-anonymous destination is a WS-Addressing - // decoupled response, and the destination came from the caller. The scheme - // and address checks in AddressingResponseEndpointPolicy ran against that - // destination only, so following a redirect would step straight past them: - // a 307 from the caller's own endpoint to http://169.254.169.254/ would be - // honoured even though neither the scheme nor the address would have - // passed. The reply is fire-and-forget, so nothing legitimate needs the - // redirect. - EndpointReference to = msgContext.getTo(); - if (msgContext.isServerSide() && to != null - && !to.hasAnonymousAddress() && !to.hasNoneAddress()) { + // Following a redirect on a decoupled response would step past the scheme + // and address checks, which only ever saw the caller-supplied destination; + // see AddressingResponseEndpointPolicy.isDecoupledResponse. + if (AddressingResponseEndpointPolicy.isDecoupledResponse(msgContext)) { requestConfig.setRedirectsEnabled(false); }
