From 4d11441d8d64d52587fed2b213de893adc63648e Mon Sep 17 00:00:00 2001 From: Colm O hEigeartaigh Date: Fri, 3 Jul 2026 10:52:53 +0100 Subject: [PATCH] Disable decoupled destinations by default for WS-Addressing --- .../cxf/ws/addressing/ContextUtils.java | 80 ++++++++++-- .../addressing/impl/InternalContextUtils.java | 24 +++- .../ws/addressing/impl/MAPAggregatorImpl.java | 115 ++++++++++++++++-- .../DecoupledDestinationProtocolTest.java | 36 +++++- .../ws/addressing/impl/MAPAggregatorTest.java | 6 + .../cxf/ws/rm/InternalContextUtils.java | 2 +- .../org/apache/cxf/ws/rm/Messages.properties | 1 + .../main/java/org/apache/cxf/ws/rm/Proxy.java | 5 + .../org/apache/cxf/ws/rm/RMInInterceptor.java | 5 + .../apache/cxf/ws/rm/RMOutInterceptor.java | 5 + .../apache/cxf/ws/rm/RMInInterceptorTest.java | 8 +- .../cxf/systest/ws/rm/DecoupledBareTest.java | 8 ++ .../ws/rm/DecoupledClientServerTest.java | 8 ++ .../WSAFaultToClientServerTest.java | 6 + .../ws/addressing/DecoupledJMSTest.java | 3 +- .../cxf/systest/ws/addressing/MAPTest.java | 10 +- .../ws/policy/AddressingInlinePolicyTest.java | 7 ++ .../policy/AddressingOptionalPolicyTest.java | 7 ++ .../ws/policy/AddressingPolicy0705Test.java | 8 ++ .../ws/policy/AddressingPolicyTest.java | 8 ++ 20 files changed, 319 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/org/apache/cxf/ws/addressing/ContextUtils.java b/core/src/main/java/org/apache/cxf/ws/addressing/ContextUtils.java index 1eed4d474c8..9648bcfb8c8 100644 --- a/core/src/main/java/org/apache/cxf/ws/addressing/ContextUtils.java +++ b/core/src/main/java/org/apache/cxf/ws/addressing/ContextUtils.java @@ -66,6 +66,30 @@ public final class ContextUtils { public static final String ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY = "org.apache.cxf.ws.addressing.decoupled.allowedSchemes"; + /** + * System property that enables WS-Addressing decoupled destinations (non-anonymous + * wsa:ReplyTo / wsa:FaultTo). Defaults to {@code false} — decoupled destinations + * are disabled by default to prevent SSRF. Set to {@code true} only when + * your deployment legitimately requires decoupled WS-Addressing callbacks. + */ + public static final String WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY = + "org.apache.cxf.ws.addressing.decoupled.enabled"; + + /** + * Exchange property key that higher-level protocols (e.g. WS-RM) may set to + * {@code Boolean.TRUE} to pre-approve decoupled wsa:ReplyTo / wsa:FaultTo + * destinations for a specific exchange, bypassing the global + * {@value #WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY} opt-in flag. + * + *

The URI-scheme allowlist ({@value #ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY} / + * {@link #DEFAULT_ALLOWED_DECOUPLED_DEST_SCHEMES}) is still enforced even when + * this flag is set, to prevent reaching dangerous URI types (e.g. {@code file://}). + * + *

Only trusted CXF modules should set this property. + */ + public static final String DECOUPLED_DESTINATION_APPROVED_PROPERTY = + "org.apache.cxf.ws.addressing.decoupled.approved"; + /** * Default set of URI scheme prefixes permitted in wsa:ReplyTo / wsa:FaultTo * decoupled-destination addresses. Schemes that can open arbitrary filesystem or @@ -590,13 +614,33 @@ public static Message createMessage(Exchange exchange) { } /** - * Returns {@code true} if the scheme of {@code uri} is permitted as a - * wsa:ReplyTo / wsa:FaultTo decoupled-destination address. The permitted set is - * read from the system property {@value #ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY} - * (comma-separated prefix list) and falls back to - * {@link #DEFAULT_ALLOWED_DECOUPLED_DEST_SCHEMES} when the property is absent. + * Returns {@code true} if {@code uri} is permitted as a wsa:ReplyTo / wsa:FaultTo + * decoupled-destination address for the WS-Addressing path. + * + *

WS-Addressing decoupled destinations are disabled by default to + * prevent SSRF: any attacker who can craft a SOAP request can otherwise force + * the server to open an outbound connection to any URL. Enable with + * {@value #WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY}{@code =true}. + * + *

When enabled, the URI must start with a prefix from + * {@value #ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY} or + * {@link #DEFAULT_ALLOWED_DECOUPLED_DEST_SCHEMES}. */ public static boolean isDecoupledDestinationAllowed(String uri) { + if (uri == null) { + return false; + } + if (!Boolean.parseBoolean( + System.getProperty(WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "false"))) { + return false; + } + return isDecoupledDestinationSchemeAllowed(uri); + } + + /** + * Validates the URI scheme for {@link #isDecoupledDestinationAllowed}. + */ + public static boolean isDecoupledDestinationSchemeAllowed(String uri) { if (uri == null) { return false; } @@ -647,11 +691,29 @@ public Conduit getBackChannel(Message inMessage) throws IOException { return null; } final String destinationUri = reference.getAddress().getValue(); - if (!isDecoupledDestinationAllowed(destinationUri)) { + boolean approved = Boolean.TRUE.equals( + inMessage.getExchange().get(DECOUPLED_DESTINATION_APPROVED_PROPERTY)); + if (approved) { + // Higher-level protocol (e.g. WS-RM) pre-approved decoupled addressing + // for this exchange; still enforce the scheme allowlist. + if (!isDecoupledDestinationSchemeAllowed(destinationUri)) { + LOG.log(Level.WARNING, + "Rejected pre-approved decoupled destination with disallowed scheme: {0}. " + + "Configure permitted URI schemes with system property {1}", + new Object[] {destinationUri, ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY}); + return null; + } + } else if (!isDecoupledDestinationAllowed(destinationUri)) { LOG.log(Level.WARNING, - "Rejected wsa:ReplyTo/FaultTo address with disallowed scheme: {0}. " - + "Override permitted schemes with system property {1}", - new Object[] {destinationUri, ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY}); + "Rejected wsa:ReplyTo/FaultTo decoupled destination: {0}. " + + "Decoupled WS-Addressing is disabled by default; " + + "enable with system property {1}=true, " + + "or configure permitted URI schemes with {2}", + new Object[] { + destinationUri, + WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, + ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY + }); return null; } Bus bus = inMessage.getExchange().getBus(); diff --git a/rt/ws/addr/src/main/java/org/apache/cxf/ws/addressing/impl/InternalContextUtils.java b/rt/ws/addr/src/main/java/org/apache/cxf/ws/addressing/impl/InternalContextUtils.java index 1caccd8bcf0..5b74976b914 100644 --- a/rt/ws/addr/src/main/java/org/apache/cxf/ws/addressing/impl/InternalContextUtils.java +++ b/rt/ws/addr/src/main/java/org/apache/cxf/ws/addressing/impl/InternalContextUtils.java @@ -95,11 +95,27 @@ public Conduit getBackChannel(Message inMessage) throws IOException { return null; } final String destinationUri = reference.getAddress().getValue(); - if (!ContextUtils.isDecoupledDestinationAllowed(destinationUri)) { + boolean approved = Boolean.TRUE.equals( + inMessage.getExchange().get(ContextUtils.DECOUPLED_DESTINATION_APPROVED_PROPERTY)); + if (approved) { + if (!ContextUtils.isDecoupledDestinationSchemeAllowed(destinationUri)) { + LOG.log(Level.WARNING, + "Rejected pre-approved decoupled destination with disallowed scheme: {0}. " + + "Configure permitted URI schemes with system property {1}", + new Object[] {destinationUri, ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY}); + return null; + } + } else if (!ContextUtils.isDecoupledDestinationAllowed(destinationUri)) { LOG.log(Level.WARNING, - "Rejected wsa:ReplyTo/FaultTo address with disallowed scheme: {0}. " - + "Override the permitted schemes with system property {1}", - new Object[] {destinationUri, ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY}); + "Rejected wsa:ReplyTo/FaultTo decoupled destination: {0}. " + + "Decoupled WS-Addressing is disabled by default; " + + "enable with system property {1}=true, " + + "and/or configure permitted URI schemes with {2}", + new Object[] { + destinationUri, + ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, + ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY + }); return null; } Bus bus = inMessage.getExchange().getBus(); diff --git a/rt/ws/addr/src/main/java/org/apache/cxf/ws/addressing/impl/MAPAggregatorImpl.java b/rt/ws/addr/src/main/java/org/apache/cxf/ws/addressing/impl/MAPAggregatorImpl.java index f33cfe43d97..321b78d7945 100644 --- a/rt/ws/addr/src/main/java/org/apache/cxf/ws/addressing/impl/MAPAggregatorImpl.java +++ b/rt/ws/addr/src/main/java/org/apache/cxf/ws/addressing/impl/MAPAggregatorImpl.java @@ -470,6 +470,9 @@ protected boolean mediate(Message message, boolean isFault) { if (isOneway || !ContextUtils.isGenericAddress(maps.getReplyTo())) { + // Fail fast before the 202 partial response is sent when the + // non-anonymous ReplyTo is blocked by the decoupled-destination guard. + assertDecoupledReplyToAllowed(message, maps); InternalContextUtils.rebaseResponse(maps.getReplyTo(), maps, message); @@ -885,18 +888,7 @@ private void addRoleSpecific(AddressingProperties maps, if (isFault && !ContextUtils.isGenericAddress(inMAPs.getFaultTo())) { - - Message m = message.getExchange().getInFaultMessage(); - if (m == null) { - m = message; - } - InternalContextUtils.rebaseResponse(inMAPs.getFaultTo(), - inMAPs, - m); - - Destination destination = InternalContextUtils.createDecoupledDestination(m.getExchange(), - inMAPs.getFaultTo()); - m.getExchange().setDestination(destination); + rebaseOrFallbackFaultTo(message, maps, inMAPs); } } } @@ -1224,6 +1216,105 @@ private void checkReplyTo(Message message, AddressingProperties maps) { } } + /** + * Throws a {@code wsa:DestinationUnreachable} SOAP fault when a non-anonymous + * {@code wsa:ReplyTo} is present but decoupled WS-Addressing is disabled. + * Must be called before {@link InternalContextUtils#rebaseResponse} so the fault + * is returned on the synchronous connection, not silently dropped after the 202. + */ + private void assertDecoupledReplyToAllowed(Message message, AddressingProperties maps) { + EndpointReferenceType replyTo = maps.getReplyTo(); + if (ContextUtils.isGenericAddress(replyTo)) { + return; + } + final String uri = replyTo.getAddress().getValue(); + + boolean approved = Boolean.TRUE.equals( + message.getExchange().get(ContextUtils.DECOUPLED_DESTINATION_APPROVED_PROPERTY)); + if (approved) { + if (ContextUtils.isDecoupledDestinationSchemeAllowed(uri)) { + return; + } + final String reason = "Decoupled WS-Addressing ReplyTo (" + uri + + ") is not permitted by this server: URI scheme is not allowed. " + + "Configure permitted schemes with system property " + + ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY; + if (isSOAP12(message)) { + SoapFault f = new SoapFault(reason, Soap12.getInstance().getSender()); + f.setSubCode(Names.DESTINATION_UNREACHABLE_QNAME); + throw f; + } + throw new SoapFault(reason, Names.DESTINATION_UNREACHABLE_QNAME); + } + + if (ContextUtils.isDecoupledDestinationAllowed(uri)) { + return; + } + final String reason = "Decoupled WS-Addressing ReplyTo (" + uri + + ") is not permitted by this server. Enable with system property " + + ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY + "=true"; + if (isSOAP12(message)) { + SoapFault f = new SoapFault(reason, Soap12.getInstance().getSender()); + f.setSubCode(Names.DESTINATION_UNREACHABLE_QNAME); + throw f; + } + throw new SoapFault(reason, Names.DESTINATION_UNREACHABLE_QNAME); + } + + /** + * Rebases the response to the non-anonymous {@code wsa:FaultTo} endpoint when + * decoupled WS-Addressing is permitted, or falls back to the synchronous + * {@code wsa:ReplyTo} with a warning when it is not. + */ + private void rebaseOrFallbackFaultTo(Message message, + AddressingProperties maps, + AddressingProperties inMAPs) { + final String faultToUri = inMAPs.getFaultTo().getAddress().getValue(); + boolean approved = Boolean.TRUE.equals( + message.getExchange().get(ContextUtils.DECOUPLED_DESTINATION_APPROVED_PROPERTY)); + if (approved) { + if (ContextUtils.isDecoupledDestinationSchemeAllowed(faultToUri)) { + Message m = message.getExchange().getInFaultMessage(); + if (m == null) { + m = message; + } + InternalContextUtils.rebaseResponse(inMAPs.getFaultTo(), inMAPs, m); + Destination destination = + InternalContextUtils.createDecoupledDestination(m.getExchange(), inMAPs.getFaultTo()); + m.getExchange().setDestination(destination); + return; + } + LOG.log(Level.WARNING, + "Decoupled pre-approved FaultTo ({0}) is not permitted: URI scheme is not allowed. " + + "Fault will be delivered to ReplyTo instead. Configure permitted schemes with {1}", + new Object[]{faultToUri, ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY}); + if (inMAPs.getReplyTo() != null) { + maps.setTo(inMAPs.getReplyTo()); + } + return; + } + if (!ContextUtils.isDecoupledDestinationAllowed(faultToUri)) { + LOG.log(Level.WARNING, + "Decoupled WS-Addressing FaultTo ({0}) is not permitted; " + + "fault will be delivered to ReplyTo instead. " + + "Enable with system property {1}=true", + new Object[]{faultToUri, + ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY}); + if (inMAPs.getReplyTo() != null) { + maps.setTo(inMAPs.getReplyTo()); + } + return; + } + Message m = message.getExchange().getInFaultMessage(); + if (m == null) { + m = message; + } + InternalContextUtils.rebaseResponse(inMAPs.getFaultTo(), inMAPs, m); + Destination destination = + InternalContextUtils.createDecoupledDestination(m.getExchange(), inMAPs.getFaultTo()); + m.getExchange().setDestination(destination); + } + private boolean isSOAP12(Message message) { if (message.getExchange().getBinding() instanceof SoapBinding) { SoapBinding binding = (SoapBinding)message.getExchange().getBinding(); diff --git a/rt/ws/addr/src/test/java/org/apache/cxf/ws/addressing/impl/DecoupledDestinationProtocolTest.java b/rt/ws/addr/src/test/java/org/apache/cxf/ws/addressing/impl/DecoupledDestinationProtocolTest.java index 98ce7592fe8..1c472c25809 100644 --- a/rt/ws/addr/src/test/java/org/apache/cxf/ws/addressing/impl/DecoupledDestinationProtocolTest.java +++ b/rt/ws/addr/src/test/java/org/apache/cxf/ws/addressing/impl/DecoupledDestinationProtocolTest.java @@ -35,6 +35,7 @@ import org.apache.cxf.ws.addressing.EndpointReferenceType; import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -61,9 +62,19 @@ */ public class DecoupledDestinationProtocolTest { + @Before + public void clearSystemPropertiesBeforeTest() { + clearDecoupledDestinationSystemProperties(); + } + @After - public void clearSystemProperty() { + public void clearSystemPropertiesAfterTest() { + clearDecoupledDestinationSystemProperties(); + } + + private static void clearDecoupledDestinationSystemProperties() { System.clearProperty(ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY); + System.clearProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY); } // ------------------------------------------------------------------------- @@ -76,6 +87,7 @@ public void clearSystemProperty() { */ @Test public void testFileSchemeReplyToIsRejected() throws Exception { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); final String attackerReplyTo = "file:///etc/passwd"; Bus bus = mock(Bus.class); @@ -106,6 +118,7 @@ public void testFileSchemeReplyToIsRejected() throws Exception { */ @Test public void testFileSchemeViaFaultToIsRejected() throws Exception { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); final String attackerFaultTo = "file:///var/log/app.log"; Bus bus = mock(Bus.class); @@ -139,6 +152,7 @@ public void testFileSchemeViaFaultToIsRejected() throws Exception { */ @Test public void testHttpSchemeReplyToIsAllowed() throws Exception { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); final String replyTo = "http://callback.example.com/reply"; Bus bus = mock(Bus.class); @@ -181,6 +195,7 @@ public void testHttpSchemeReplyToIsAllowed() throws Exception { */ @Test public void testJmsSchemeReplyToIsAllowed() throws Exception { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); final String replyTo = "jms:queue:replies?timeToLive=1000"; Bus bus = mock(Bus.class); @@ -222,6 +237,7 @@ public void testJmsSchemeReplyToIsAllowed() throws Exception { */ @Test public void testFileSchemeAllowedViaSystemProperty() throws Exception { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); System.setProperty( ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY, "file://,http://"); @@ -263,6 +279,7 @@ public void testFileSchemeAllowedViaSystemProperty() throws Exception { */ @Test public void testSystemPropertyReplacesDefaults() throws Exception { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); System.setProperty( ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY, "jms:"); @@ -296,6 +313,7 @@ public void testSystemPropertyReplacesDefaults() throws Exception { @Test public void testIsReplyToSchemeAllowedDefaults() { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); // Allowed by default for (String allowed : ContextUtils.DEFAULT_ALLOWED_DECOUPLED_DEST_SCHEMES) { assertEquals("Expected allowed: " + allowed, @@ -316,6 +334,7 @@ public void testIsReplyToSchemeAllowedDefaults() { */ @Test public void testEmptyPropertyTokensDoNotBypassAllowlist() { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); // Leading comma — produces an empty first token System.setProperty(ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY, ",http://"); @@ -356,6 +375,7 @@ public void testEmptyPropertyTokensDoNotBypassAllowlist() { */ @Test public void testContextUtilsFaultToFileSchemeIsRejected() throws Exception { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); final String attackerFaultTo = "file:///etc/passwd"; Bus bus = mock(Bus.class); @@ -386,6 +406,7 @@ public void testContextUtilsFaultToFileSchemeIsRejected() throws Exception { */ @Test public void testContextUtilsFaultToHttpSchemeIsAllowed() throws Exception { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); final String faultTo = "http://callback.example.com/fault"; Bus bus = mock(Bus.class); @@ -417,6 +438,19 @@ public void testContextUtilsFaultToHttpSchemeIsAllowed() throws Exception { assertSame(mockConduit, backChannel); } + /** + * With no system properties set, ALL decoupled destinations must be rejected + * regardless of scheme. This is the new secure-by-default behaviour. + */ + @Test + public void testDecoupledAddressingDisabledByDefault() { + assertEquals(false, ContextUtils.isDecoupledDestinationAllowed("http://callback.example.com/")); + assertEquals(false, ContextUtils.isDecoupledDestinationAllowed("https://callback.example.com/")); + assertEquals(false, ContextUtils.isDecoupledDestinationAllowed("jms:queue:replies")); + assertEquals(false, ContextUtils.isDecoupledDestinationAllowed("file:///etc/passwd")); + assertEquals(false, ContextUtils.isDecoupledDestinationAllowed(null)); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- diff --git a/rt/ws/addr/src/test/java/org/apache/cxf/ws/addressing/impl/MAPAggregatorTest.java b/rt/ws/addr/src/test/java/org/apache/cxf/ws/addressing/impl/MAPAggregatorTest.java index 8118ed1c7ee..0fdd917d1d8 100644 --- a/rt/ws/addr/src/test/java/org/apache/cxf/ws/addressing/impl/MAPAggregatorTest.java +++ b/rt/ws/addr/src/test/java/org/apache/cxf/ws/addressing/impl/MAPAggregatorTest.java @@ -103,11 +103,17 @@ public class MAPAggregatorTest { @Before public void setUp() { + // Tests in this class exercise both anonymous and non-anonymous (decoupled) + // WS-Addressing destinations. Enable decoupled so the legitimate decoupled + // tests are not rejected by the SSRF guard. + System.setProperty( + ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); aggregator = new MAPAggregatorImpl(); } @After public void tearDown() { + System.clearProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY); expectedMAPs = null; expectedTo = null; expectedReplyTo = null; diff --git a/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/InternalContextUtils.java b/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/InternalContextUtils.java index e6d2220b657..889a5246e38 100644 --- a/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/InternalContextUtils.java +++ b/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/InternalContextUtils.java @@ -79,7 +79,7 @@ public Conduit getBackChannel(Message inMessage) throws IOException { return null; } final String destinationUri = reference.getAddress().getValue(); - if (!ContextUtils.isDecoupledDestinationAllowed(destinationUri)) { + if (!ContextUtils.isDecoupledDestinationSchemeAllowed(destinationUri)) { return null; } Bus bus = inMessage.getExchange().getBus(); diff --git a/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/Messages.properties b/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/Messages.properties index 82661b57351..10b9acdd19f 100644 --- a/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/Messages.properties +++ b/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/Messages.properties @@ -24,6 +24,7 @@ RM_INVOCATION_FAILED = Invocation of RM protocol operation failed. SEQ_TERMINATION_FAILURE = Failed to terminate sequence {0}. STANDALONE_ANON_ACKS_NOT_SUPPORTED = It is not possible to send out-of-band acknowledgments to the anonymous address.\nAn acknowledgement will be piggybacked on the next response. +WSRM_ACKS_TO_SCHEME_NOT_PERMITTED = WS-RM standalone acknowledgement to "{0}" was blocked: the URI scheme is not in the permitted list. Configure permitted schemes with system property org.apache.cxf.ws.addressing.decoupled.allowedSchemes. STANDALONE_CLOSE_SEQUENCE_NO_TARGET_MSG = No target address to send out-of-band close sequence to. STANDALONE_CLOSE_SEQUENCE_ANON_TARGET_MSG = It is not possible to send an out-of-band close sequence to the anonymous address. STANDALONE_ANON_TERMINATE_SEQUENCE_MSG = It is not possible to send out-of-band terminate sequence message to the anonymous address.\nIt will be piggybacked on the next response. diff --git a/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/Proxy.java b/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/Proxy.java index 44f19941048..3c6f54f4ae7 100644 --- a/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/Proxy.java +++ b/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/Proxy.java @@ -49,6 +49,7 @@ import org.apache.cxf.transport.Conduit; import org.apache.cxf.workqueue.SynchronousExecutor; import org.apache.cxf.ws.addressing.AttributedURIType; +import org.apache.cxf.ws.addressing.ContextUtils; import org.apache.cxf.ws.addressing.EndpointReferenceType; import org.apache.cxf.ws.addressing.MAPAggregator; import org.apache.cxf.ws.addressing.RelatesToType; @@ -92,6 +93,10 @@ void acknowledge(DestinationSequence ds) throws RMException { LOG.log(Level.WARNING, "STANDALONE_ANON_ACKS_NOT_SUPPORTED"); return; } + if (!ContextUtils.isDecoupledDestinationSchemeAllowed(address)) { + LOG.log(Level.WARNING, "WSRM_ACKS_TO_SCHEME_NOT_PERMITTED", address); + return; + } RMConstants constants = protocol.getConstants(); OperationInfo oi = reliableEndpoint.getEndpoint(protocol).getEndpointInfo() .getService().getInterface().getOperation(constants.getSequenceAckOperationName()); diff --git a/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/RMInInterceptor.java b/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/RMInInterceptor.java index 6438c473fd5..630962d87f5 100644 --- a/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/RMInInterceptor.java +++ b/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/RMInInterceptor.java @@ -161,6 +161,11 @@ protected void handle(Message message) throws SequenceFault, RMException { return; } + // Pre-approve decoupled wsa:ReplyTo/wsa:FaultTo destinations for this exchange. + // MAPAggregator performs the early fail-fast check; this exchange flag ensures + // the later decoupled backchannel creation path honors RM callbacks as well. + message.getExchange().put(ContextUtils.DECOUPLED_DESTINATION_APPROVED_PROPERTY, Boolean.TRUE); + Object originalRequestor = message.get(RMMessageConstants.ORIGINAL_REQUESTOR_ROLE); if (null != originalRequestor) { LOG.fine("Restoring original requestor role to: " + originalRequestor); diff --git a/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/RMOutInterceptor.java b/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/RMOutInterceptor.java index 90f082016df..c576c7ac5e8 100644 --- a/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/RMOutInterceptor.java +++ b/rt/ws/rm/src/main/java/org/apache/cxf/ws/rm/RMOutInterceptor.java @@ -65,6 +65,11 @@ protected void handle(Message msg) throws SequenceFault, RMException { return; } + // Pre-approve decoupled wsa:ReplyTo/wsa:FaultTo destinations for this exchange. + // WS-RM requires decoupled addressing for async acknowledgements and responses; + // the ContextUtils guard is bypassed at exchange level rather than globally. + msg.getExchange().put(ContextUtils.DECOUPLED_DESTINATION_APPROVED_PROPERTY, Boolean.TRUE); + RMConfiguration config = getManager().getEffectiveConfiguration(msg); String wsaNamespace = config.getAddressingNamespace(); String rmNamespace = config.getRMNamespace(); diff --git a/rt/ws/rm/src/test/java/org/apache/cxf/ws/rm/RMInInterceptorTest.java b/rt/ws/rm/src/test/java/org/apache/cxf/ws/rm/RMInInterceptorTest.java index 00970c3f593..42c93f45d5c 100644 --- a/rt/ws/rm/src/test/java/org/apache/cxf/ws/rm/RMInInterceptorTest.java +++ b/rt/ws/rm/src/test/java/org/apache/cxf/ws/rm/RMInInterceptorTest.java @@ -100,7 +100,7 @@ public void testHandleCreateSequenceOnServer() throws SequenceFault, RMException interceptor.handle(message); verify(rme, times(1)).receivedControlMessage(); - verify(message, times(2)).getExchange(); + verify(message, times(3)).getExchange(); } @Test @@ -118,7 +118,7 @@ public void testHandleCreateSequenceOnClient() throws SequenceFault, RMException interceptor.handle(message); verify(rme, times(1)).receivedControlMessage(); verify(proxy, times(1)).createSequenceResponse(csr, ProtocolVariation.RM10WSA200408); - verify(message, times(2)).getExchange(); + verify(message, times(3)).getExchange(); } @Test @@ -140,7 +140,7 @@ private void testHandleSequenceAck(boolean onServer) interceptor.handle(message); verify(rme, times(1)).receivedControlMessage(); verify(interceptor, times(1)).processAcknowledgments(rme, rmps, ProtocolVariation.RM10WSA200408); - verify(message, times(2)).getExchange(); + verify(message, times(3)).getExchange(); } @Test @@ -159,7 +159,7 @@ private void testHandleTerminateSequence(boolean onServer) throws SequenceFault, when(message.get(AssertionInfoMap.class)).thenReturn(null); interceptor.handle(message); - verify(message, times(2)).getExchange(); + verify(message, times(3)).getExchange(); verify(rme, times(1)).receivedControlMessage(); } diff --git a/systests/ws-rm/src/test/java/org/apache/cxf/systest/ws/rm/DecoupledBareTest.java b/systests/ws-rm/src/test/java/org/apache/cxf/systest/ws/rm/DecoupledBareTest.java index def4a8b5f7a..54e9101686a 100644 --- a/systests/ws-rm/src/test/java/org/apache/cxf/systest/ws/rm/DecoupledBareTest.java +++ b/systests/ws-rm/src/test/java/org/apache/cxf/systest/ws/rm/DecoupledBareTest.java @@ -33,11 +33,13 @@ import org.apache.cxf.systest.ws.util.ConnectionHelper; import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase; import org.apache.cxf.testutil.common.AbstractBusTestServerBase; +import org.apache.cxf.ws.addressing.ContextUtils; import org.apache.hello_world_soap_http.DocLitBare; import org.apache.hello_world_soap_http.DocLitBareGreeterImpl; import org.apache.hello_world_soap_http.SOAPServiceAddressingDocLitBare; import org.apache.hello_world_soap_http.types.BareDocumentResponse; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -81,9 +83,15 @@ public void tearDown() { @BeforeClass public static void startServers() throws Exception { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); assertTrue("server did not launch correctly", launchServer(DecoupledBareServer.class, true)); } + @AfterClass + public static void cleanup() throws Exception { + System.clearProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY); + } + @Test public void testDecoupled() throws Exception { SpringBusFactory bf = new SpringBusFactory(); diff --git a/systests/ws-rm/src/test/java/org/apache/cxf/systest/ws/rm/DecoupledClientServerTest.java b/systests/ws-rm/src/test/java/org/apache/cxf/systest/ws/rm/DecoupledClientServerTest.java index 631a8f663e5..2e8793fa727 100644 --- a/systests/ws-rm/src/test/java/org/apache/cxf/systest/ws/rm/DecoupledClientServerTest.java +++ b/systests/ws-rm/src/test/java/org/apache/cxf/systest/ws/rm/DecoupledClientServerTest.java @@ -37,7 +37,9 @@ import org.apache.cxf.systest.ws.util.ConnectionHelper; import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase; import org.apache.cxf.testutil.common.AbstractBusTestServerBase; +import org.apache.cxf.ws.addressing.ContextUtils; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -89,10 +91,16 @@ public void tearDown() { @BeforeClass public static void startServers() throws Exception { + System.setProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "true"); assertTrue("server did not launch correctly", launchServer(DecoupledServer.class, true)); } + @AfterClass + public static void cleanup() throws Exception { + System.clearProperty(ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY); + } + @Test public void testDecoupled() throws Exception { SpringBusFactory bf = new SpringBusFactory(); diff --git a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addr_feature/WSAFaultToClientServerTest.java b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addr_feature/WSAFaultToClientServerTest.java index cb373c4a0d6..81822161ebd 100644 --- a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addr_feature/WSAFaultToClientServerTest.java +++ b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addr_feature/WSAFaultToClientServerTest.java @@ -38,6 +38,7 @@ import org.apache.hello_world_soap_http.Greeter; import org.apache.hello_world_soap_http.SOAPService; +import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -55,9 +56,14 @@ public void setUp() throws Exception { @BeforeClass public static void startServers() throws Exception { + System.setProperty("org.apache.cxf.ws.addressing.decoupled.enabled", "true"); assertTrue("FaultTo server did not launch correctly", launchServer(FaultToEndpointServer.class, true)); } + @AfterClass + public static void cleanup() throws Exception { + System.clearProperty("org.apache.cxf.ws.addressing.decoupled.enabled"); + } @Test public void testOneWayFaultTo() throws Exception { diff --git a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addressing/DecoupledJMSTest.java b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addressing/DecoupledJMSTest.java index c127db6db77..a45c1d9a8d0 100644 --- a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addressing/DecoupledJMSTest.java +++ b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addressing/DecoupledJMSTest.java @@ -77,7 +77,8 @@ public static void startServers() throws Exception { props, null)); assertTrue("server did not launch correctly", - launchServer(Server.class, null, + launchServer(Server.class, + Map.of("org.apache.cxf.ws.addressing.decoupled.enabled", "true"), new String[] {ADDRESS, GreeterImpl.class.getName()}, false)); } diff --git a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addressing/MAPTest.java b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addressing/MAPTest.java index 5843e092197..3c3711ae632 100644 --- a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addressing/MAPTest.java +++ b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/addressing/MAPTest.java @@ -21,11 +21,12 @@ import jakarta.jws.WebService; - +import org.junit.AfterClass; import org.junit.BeforeClass; import static org.junit.Assert.assertTrue; + /** * Tests the addition of WS-Addressing Message Addressing Properties. */ @@ -54,10 +55,17 @@ public String getPort() { @BeforeClass public static void startServers() throws Exception { + System.setProperty("org.apache.cxf.ws.addressing.decoupled.enabled", "true"); assertTrue("server did not launch correctly", launchServer(Server.class, null, new String[] {ADDRESS, GreeterImpl.class.getName()}, true)); } + + @AfterClass + public static void cleanup() throws Exception { + System.clearProperty("org.apache.cxf.ws.addressing.decoupled.enabled"); + } + @WebService(serviceName = "SOAPServiceAddressing", portName = "SoapPort", endpointInterface = "org.apache.hello_world_soap_http.Greeter", diff --git a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingInlinePolicyTest.java b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingInlinePolicyTest.java index 251195e9df4..2d6e8f4ad16 100644 --- a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingInlinePolicyTest.java +++ b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingInlinePolicyTest.java @@ -40,6 +40,7 @@ import org.apache.cxf.ws.policy.PolicyInInterceptor; import org.apache.cxf.ws.policy.PolicyOutInterceptor; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -92,9 +93,15 @@ public static void main(String[] args) { @BeforeClass public static void startServers() throws Exception { TestUtil.getNewPortNumber("decoupled"); + System.setProperty("org.apache.cxf.ws.addressing.decoupled.enabled", "true"); assertTrue("server did not launch correctly", launchServer(AddressingInlinePolicyServer.class, true)); } + @AfterClass + public static void cleanup() throws Exception { + System.clearProperty("org.apache.cxf.ws.addressing.decoupled.enabled"); + } + @Test public void testUsingAddressing() throws Exception { diff --git a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingOptionalPolicyTest.java b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingOptionalPolicyTest.java index 66e2963acc6..e6a97ea24e7 100644 --- a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingOptionalPolicyTest.java +++ b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingOptionalPolicyTest.java @@ -44,6 +44,7 @@ import org.apache.cxf.ws.policy.selector.MinimalAlternativeSelector; import org.apache.cxf.ws.rm.RMUtils; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -113,12 +114,18 @@ public static void main(String[] args) { @BeforeClass public static void startServers() throws Exception { TestUtil.getNewPortNumber("decoupled"); + System.setProperty("org.apache.cxf.ws.addressing.decoupled.enabled", "true"); PolicyTestHelper.updatePolicyRef("addr-optional-external.xml", ":9020", ":" + PORT); System.setProperty("temp.location", TEMPDIR); assertTrue("server did not launch correctly", launchServer(AddressingOptionalPolicyServer.class, null, new String[] {TEMPDIR})); } + @AfterClass + public static void cleanup() throws Exception { + System.clearProperty("temp.location"); + System.clearProperty("org.apache.cxf.ws.addressing.decoupled.enabled"); + } @Test public void testUsingAddressing() throws Exception { diff --git a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingPolicy0705Test.java b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingPolicy0705Test.java index 6ad442b6fe5..373ba19476d 100644 --- a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingPolicy0705Test.java +++ b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingPolicy0705Test.java @@ -38,6 +38,7 @@ import org.apache.cxf.testutil.common.AbstractBusTestServerBase; import org.apache.cxf.testutil.common.TestUtil; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -111,11 +112,18 @@ public static void startServers() throws Exception { TestUtil.getNewPortNumber("decoupled"); PolicyTestHelper.updatePolicyRef("addr-external0705.xml", ":9020", ":" + PORT); System.setProperty("temp.location", TEMPDIR); + System.setProperty("org.apache.cxf.ws.addressing.decoupled.enabled", "true"); assertTrue("server did not launch correctly", launchServer(AddressingPolicy0705Server.class, null, new String[] {TEMPDIR}, true)); } + @AfterClass + public static void cleanup() throws Exception { + System.clearProperty("temp.location"); + System.clearProperty("org.apache.cxf.ws.addressing.decoupled.enabled"); + } + @Test public void testUsingAddressing() throws Exception { SpringBusFactory bf = new SpringBusFactory(); diff --git a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingPolicyTest.java b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingPolicyTest.java index 15fa2e48e53..1d6bd6241d2 100644 --- a/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingPolicyTest.java +++ b/systests/ws-specs/src/test/java/org/apache/cxf/systest/ws/policy/AddressingPolicyTest.java @@ -38,6 +38,7 @@ import org.apache.cxf.testutil.common.AbstractBusTestServerBase; import org.apache.cxf.testutil.common.TestUtil; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -109,11 +110,18 @@ public static void startServers() throws Exception { TestUtil.getNewPortNumber("decoupled"); PolicyTestHelper.updatePolicyRef("addr-external.xml", ":9020", ":" + PORT); System.setProperty("temp.location", TEMPDIR); + System.setProperty("org.apache.cxf.ws.addressing.decoupled.enabled", "true"); assertTrue("server did not launch correctly", launchServer(AddressingPolicyServer.class, null, new String[] {TEMPDIR}, true)); } + @AfterClass + public static void cleanup() throws Exception { + System.clearProperty("temp.location"); + System.clearProperty("org.apache.cxf.ws.addressing.decoupled.enabled"); + } + @Test public void testUsingAddressing() throws Exception { SpringBusFactory bf = new SpringBusFactory();