DecoupledDestinationProtocolTest.java
/**
* 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.cxf.ws.rm;
import org.apache.cxf.Bus;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.ExchangeImpl;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageImpl;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.transport.Conduit;
import org.apache.cxf.transport.ConduitInitiator;
import org.apache.cxf.transport.ConduitInitiatorManager;
import org.apache.cxf.transport.Destination;
import org.apache.cxf.ws.addressing.AttributedURIType;
import org.apache.cxf.ws.addressing.ContextUtils;
import org.apache.cxf.ws.addressing.EndpointReferenceType;
import org.junit.After;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Verifies that the WS-RM decoupled-destination scheme allowlist prevents
* SSRF via attacker-controlled wsa:ReplyTo / wsa:FaultTo addresses carried
* in WS-RM messages. The guard is {@link ContextUtils#isDecoupledDestinationAllowed}
* called from {@link InternalContextUtils}'s internal {@code DecoupledDestination}.
*/
public class DecoupledDestinationProtocolTest {
@After
public void clearSystemProperty() {
System.clearProperty(ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY);
}
// -------------------------------------------------------------------------
// Blocked by default
// -------------------------------------------------------------------------
@Test
public void testFileSchemeIsRejected() throws Exception {
Bus bus = mock(Bus.class);
ConduitInitiatorManager cim = mock(ConduitInitiatorManager.class);
when(bus.getExtension(ConduitInitiatorManager.class)).thenReturn(cim);
Exchange exchange = buildExchange(bus);
Message inMessage = new MessageImpl();
inMessage.setExchange(exchange);
Destination dest = InternalContextUtils.createDecoupledDestination(
exchange, buildEpr("file:///etc/passwd"));
Conduit backChannel = dest.getBackChannel(inMessage);
assertNull("file:// must be blocked", backChannel);
verify(cim, never()).getConduitInitiatorForUri(any());
}
@Test
public void testCorbaSchemeIsRejected() throws Exception {
Bus bus = mock(Bus.class);
ConduitInitiatorManager cim = mock(ConduitInitiatorManager.class);
when(bus.getExtension(ConduitInitiatorManager.class)).thenReturn(cim);
Exchange exchange = buildExchange(bus);
Message inMessage = new MessageImpl();
inMessage.setExchange(exchange);
Destination dest = InternalContextUtils.createDecoupledDestination(
exchange, buildEpr("corba:NameService"));
Conduit backChannel = dest.getBackChannel(inMessage);
assertNull("corba: must be blocked", backChannel);
verify(cim, never()).getConduitInitiatorForUri(any());
}
// -------------------------------------------------------------------------
// Allowed by default
// -------------------------------------------------------------------------
@Test
public void testHttpSchemeIsAllowed() throws Exception {
final String replyTo = "http://callback.example.com/rm/reply";
Bus bus = mock(Bus.class);
ConduitInitiatorManager cim = mock(ConduitInitiatorManager.class);
ConduitInitiator initiator = mock(ConduitInitiator.class);
Conduit mockConduit = mock(Conduit.class);
when(bus.getExtension(ConduitInitiatorManager.class)).thenReturn(cim);
when(cim.getConduitInitiatorForUri(replyTo)).thenReturn(initiator);
when(initiator.getConduit(
any(EndpointInfo.class), any(EndpointReferenceType.class), any(Bus.class)))
.thenReturn(mockConduit);
Exchange exchange = buildExchange(bus);
Message inMessage = new MessageImpl();
inMessage.setExchange(exchange);
Destination dest = InternalContextUtils.createDecoupledDestination(
exchange, buildEpr(replyTo));
Conduit backChannel = dest.getBackChannel(inMessage);
assertNotNull("http:// must be allowed", backChannel);
assertSame(mockConduit, backChannel);
ArgumentCaptor<EndpointReferenceType> eprCaptor =
ArgumentCaptor.forClass(EndpointReferenceType.class);
verify(initiator).getConduit(
any(EndpointInfo.class), eprCaptor.capture(), any(Bus.class));
assertEquals(replyTo, eprCaptor.getValue().getAddress().getValue());
}
@Test
public void testHttpsSchemeIsAllowed() throws Exception {
final String replyTo = "https://callback.example.com/rm/reply";
Bus bus = mock(Bus.class);
ConduitInitiatorManager cim = mock(ConduitInitiatorManager.class);
ConduitInitiator initiator = mock(ConduitInitiator.class);
Conduit mockConduit = mock(Conduit.class);
when(bus.getExtension(ConduitInitiatorManager.class)).thenReturn(cim);
when(cim.getConduitInitiatorForUri(replyTo)).thenReturn(initiator);
when(initiator.getConduit(
any(EndpointInfo.class), any(EndpointReferenceType.class), any(Bus.class)))
.thenReturn(mockConduit);
Exchange exchange = buildExchange(bus);
Message inMessage = new MessageImpl();
inMessage.setExchange(exchange);
Destination dest = InternalContextUtils.createDecoupledDestination(
exchange, buildEpr(replyTo));
Conduit backChannel = dest.getBackChannel(inMessage);
assertNotNull("https:// must be allowed", backChannel);
assertSame(mockConduit, backChannel);
}
// -------------------------------------------------------------------------
// System-property override
// -------------------------------------------------------------------------
@Test
public void testSystemPropertyOverrideAllowsFile() throws Exception {
System.setProperty(
ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY, "file://,http://");
final String replyTo = "file:///some/permitted/path";
Bus bus = mock(Bus.class);
ConduitInitiatorManager cim = mock(ConduitInitiatorManager.class);
ConduitInitiator initiator = mock(ConduitInitiator.class);
Conduit mockConduit = mock(Conduit.class);
when(bus.getExtension(ConduitInitiatorManager.class)).thenReturn(cim);
when(cim.getConduitInitiatorForUri(replyTo)).thenReturn(initiator);
when(initiator.getConduit(
any(EndpointInfo.class), any(EndpointReferenceType.class), any(Bus.class)))
.thenReturn(mockConduit);
Exchange exchange = buildExchange(bus);
Message inMessage = new MessageImpl();
inMessage.setExchange(exchange);
Destination dest = InternalContextUtils.createDecoupledDestination(
exchange, buildEpr(replyTo));
Conduit backChannel = dest.getBackChannel(inMessage);
assertNotNull("file:// must be allowed when explicitly listed in the system property",
backChannel);
}
@Test
public void testSystemPropertyReplacesDefaults() throws Exception {
System.setProperty(
ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY, "jms:");
Bus bus = mock(Bus.class);
ConduitInitiatorManager cim = mock(ConduitInitiatorManager.class);
when(bus.getExtension(ConduitInitiatorManager.class)).thenReturn(cim);
Exchange exchange = buildExchange(bus);
Message inMessage = new MessageImpl();
inMessage.setExchange(exchange);
Destination dest = InternalContextUtils.createDecoupledDestination(
exchange, buildEpr("http://callback.example.com/rm/reply"));
Conduit backChannel = dest.getBackChannel(inMessage);
assertNull("http:// must be blocked when absent from the system property", backChannel);
verify(cim, never()).getConduitInitiatorForUri(any());
}
/**
* Empty tokens from leading/trailing/consecutive commas in the system property
* must not bypass the SSRF allowlist. An empty prefix makes
* {@code uri.startsWith("")} true for every URI.
*/
@Test
public void testEmptyPropertyTokensDoNotBypassAllowlist() throws Exception {
Bus bus = mock(Bus.class);
ConduitInitiatorManager cim = mock(ConduitInitiatorManager.class);
when(bus.getExtension(ConduitInitiatorManager.class)).thenReturn(cim);
Exchange exchange = buildExchange(bus);
Message inMessage = new MessageImpl();
inMessage.setExchange(exchange);
// Leading comma: ",http://" ��� empty first token must not allow file://
System.setProperty(ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY, ",http://");
Destination dest = InternalContextUtils.createDecoupledDestination(
exchange, buildEpr("file:///etc/passwd"));
assertNull("leading comma must not allow file:// via empty prefix bypass",
dest.getBackChannel(inMessage));
verify(cim, never()).getConduitInitiatorForUri(any());
// All-comma value: every token is empty; nothing should be allowed
System.setProperty(ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY, ",,,");
dest = InternalContextUtils.createDecoupledDestination(
exchange, buildEpr("http://callback.example.com/"));
assertNull("all-comma property must not allow any URI",
dest.getBackChannel(inMessage));
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
private static Exchange buildExchange(Bus bus) {
Endpoint endpoint = mock(Endpoint.class);
when(endpoint.getEndpointInfo()).thenReturn(new EndpointInfo());
Exchange exchange = new ExchangeImpl();
exchange.put(Bus.class, bus);
exchange.put(Endpoint.class, endpoint);
return exchange;
}
private static EndpointReferenceType buildEpr(String uri) {
AttributedURIType address = new AttributedURIType();
address.setValue(uri);
EndpointReferenceType epr = new EndpointReferenceType();
epr.setAddress(address);
return epr;
}
}