Skip to content

Commit 24a45ef

Browse files
authored
Disable decoupled destinations by default for WS-Addressing (#3279)
1 parent 07ed95f commit 24a45ef

20 files changed

Lines changed: 319 additions & 33 deletions

File tree

core/src/main/java/org/apache/cxf/ws/addressing/ContextUtils.java

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,30 @@ public final class ContextUtils {
6666
public static final String ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY =
6767
"org.apache.cxf.ws.addressing.decoupled.allowedSchemes";
6868

69+
/**
70+
* System property that enables WS-Addressing decoupled destinations (non-anonymous
71+
* wsa:ReplyTo / wsa:FaultTo). Defaults to {@code false} — decoupled destinations
72+
* are <em>disabled</em> by default to prevent SSRF. Set to {@code true} only when
73+
* your deployment legitimately requires decoupled WS-Addressing callbacks.
74+
*/
75+
public static final String WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY =
76+
"org.apache.cxf.ws.addressing.decoupled.enabled";
77+
78+
/**
79+
* Exchange property key that higher-level protocols (e.g. WS-RM) may set to
80+
* {@code Boolean.TRUE} to pre-approve decoupled wsa:ReplyTo / wsa:FaultTo
81+
* destinations for a specific exchange, bypassing the global
82+
* {@value #WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY} opt-in flag.
83+
*
84+
* <p>The URI-scheme allowlist ({@value #ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY} /
85+
* {@link #DEFAULT_ALLOWED_DECOUPLED_DEST_SCHEMES}) is still enforced even when
86+
* this flag is set, to prevent reaching dangerous URI types (e.g. {@code file://}).
87+
*
88+
* <p>Only trusted CXF modules should set this property.
89+
*/
90+
public static final String DECOUPLED_DESTINATION_APPROVED_PROPERTY =
91+
"org.apache.cxf.ws.addressing.decoupled.approved";
92+
6993
/**
7094
* Default set of URI scheme prefixes permitted in wsa:ReplyTo / wsa:FaultTo
7195
* decoupled-destination addresses. Schemes that can open arbitrary filesystem or
@@ -590,13 +614,33 @@ public static Message createMessage(Exchange exchange) {
590614
}
591615

592616
/**
593-
* Returns {@code true} if the scheme of {@code uri} is permitted as a
594-
* wsa:ReplyTo / wsa:FaultTo decoupled-destination address. The permitted set is
595-
* read from the system property {@value #ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY}
596-
* (comma-separated prefix list) and falls back to
597-
* {@link #DEFAULT_ALLOWED_DECOUPLED_DEST_SCHEMES} when the property is absent.
617+
* Returns {@code true} if {@code uri} is permitted as a wsa:ReplyTo / wsa:FaultTo
618+
* decoupled-destination address for the <em>WS-Addressing</em> path.
619+
*
620+
* <p>WS-Addressing decoupled destinations are <em>disabled by default</em> to
621+
* prevent SSRF: any attacker who can craft a SOAP request can otherwise force
622+
* the server to open an outbound connection to any URL. Enable with
623+
* {@value #WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY}{@code =true}.
624+
*
625+
* <p>When enabled, the URI must start with a prefix from
626+
* {@value #ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY} or
627+
* {@link #DEFAULT_ALLOWED_DECOUPLED_DEST_SCHEMES}.
598628
*/
599629
public static boolean isDecoupledDestinationAllowed(String uri) {
630+
if (uri == null) {
631+
return false;
632+
}
633+
if (!Boolean.parseBoolean(
634+
System.getProperty(WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY, "false"))) {
635+
return false;
636+
}
637+
return isDecoupledDestinationSchemeAllowed(uri);
638+
}
639+
640+
/**
641+
* Validates the URI scheme for {@link #isDecoupledDestinationAllowed}.
642+
*/
643+
public static boolean isDecoupledDestinationSchemeAllowed(String uri) {
600644
if (uri == null) {
601645
return false;
602646
}
@@ -647,11 +691,29 @@ public Conduit getBackChannel(Message inMessage) throws IOException {
647691
return null;
648692
}
649693
final String destinationUri = reference.getAddress().getValue();
650-
if (!isDecoupledDestinationAllowed(destinationUri)) {
694+
boolean approved = Boolean.TRUE.equals(
695+
inMessage.getExchange().get(DECOUPLED_DESTINATION_APPROVED_PROPERTY));
696+
if (approved) {
697+
// Higher-level protocol (e.g. WS-RM) pre-approved decoupled addressing
698+
// for this exchange; still enforce the scheme allowlist.
699+
if (!isDecoupledDestinationSchemeAllowed(destinationUri)) {
700+
LOG.log(Level.WARNING,
701+
"Rejected pre-approved decoupled destination with disallowed scheme: {0}. "
702+
+ "Configure permitted URI schemes with system property {1}",
703+
new Object[] {destinationUri, ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY});
704+
return null;
705+
}
706+
} else if (!isDecoupledDestinationAllowed(destinationUri)) {
651707
LOG.log(Level.WARNING,
652-
"Rejected wsa:ReplyTo/FaultTo address with disallowed scheme: {0}. "
653-
+ "Override permitted schemes with system property {1}",
654-
new Object[] {destinationUri, ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY});
708+
"Rejected wsa:ReplyTo/FaultTo decoupled destination: {0}. "
709+
+ "Decoupled WS-Addressing is disabled by default; "
710+
+ "enable with system property {1}=true, "
711+
+ "or configure permitted URI schemes with {2}",
712+
new Object[] {
713+
destinationUri,
714+
WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY,
715+
ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY
716+
});
655717
return null;
656718
}
657719
Bus bus = inMessage.getExchange().getBus();

rt/ws/addr/src/main/java/org/apache/cxf/ws/addressing/impl/InternalContextUtils.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,27 @@ public Conduit getBackChannel(Message inMessage) throws IOException {
9595
return null;
9696
}
9797
final String destinationUri = reference.getAddress().getValue();
98-
if (!ContextUtils.isDecoupledDestinationAllowed(destinationUri)) {
98+
boolean approved = Boolean.TRUE.equals(
99+
inMessage.getExchange().get(ContextUtils.DECOUPLED_DESTINATION_APPROVED_PROPERTY));
100+
if (approved) {
101+
if (!ContextUtils.isDecoupledDestinationSchemeAllowed(destinationUri)) {
102+
LOG.log(Level.WARNING,
103+
"Rejected pre-approved decoupled destination with disallowed scheme: {0}. "
104+
+ "Configure permitted URI schemes with system property {1}",
105+
new Object[] {destinationUri, ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY});
106+
return null;
107+
}
108+
} else if (!ContextUtils.isDecoupledDestinationAllowed(destinationUri)) {
99109
LOG.log(Level.WARNING,
100-
"Rejected wsa:ReplyTo/FaultTo address with disallowed scheme: {0}. "
101-
+ "Override the permitted schemes with system property {1}",
102-
new Object[] {destinationUri, ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY});
110+
"Rejected wsa:ReplyTo/FaultTo decoupled destination: {0}. "
111+
+ "Decoupled WS-Addressing is disabled by default; "
112+
+ "enable with system property {1}=true, "
113+
+ "and/or configure permitted URI schemes with {2}",
114+
new Object[] {
115+
destinationUri,
116+
ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY,
117+
ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY
118+
});
103119
return null;
104120
}
105121
Bus bus = inMessage.getExchange().getBus();

rt/ws/addr/src/main/java/org/apache/cxf/ws/addressing/impl/MAPAggregatorImpl.java

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,9 @@ protected boolean mediate(Message message, boolean isFault) {
470470

471471
if (isOneway
472472
|| !ContextUtils.isGenericAddress(maps.getReplyTo())) {
473+
// Fail fast before the 202 partial response is sent when the
474+
// non-anonymous ReplyTo is blocked by the decoupled-destination guard.
475+
assertDecoupledReplyToAllowed(message, maps);
473476
InternalContextUtils.rebaseResponse(maps.getReplyTo(),
474477
maps,
475478
message);
@@ -885,18 +888,7 @@ private void addRoleSpecific(AddressingProperties maps,
885888

886889
if (isFault
887890
&& !ContextUtils.isGenericAddress(inMAPs.getFaultTo())) {
888-
889-
Message m = message.getExchange().getInFaultMessage();
890-
if (m == null) {
891-
m = message;
892-
}
893-
InternalContextUtils.rebaseResponse(inMAPs.getFaultTo(),
894-
inMAPs,
895-
m);
896-
897-
Destination destination = InternalContextUtils.createDecoupledDestination(m.getExchange(),
898-
inMAPs.getFaultTo());
899-
m.getExchange().setDestination(destination);
891+
rebaseOrFallbackFaultTo(message, maps, inMAPs);
900892
}
901893
}
902894
}
@@ -1224,6 +1216,105 @@ private void checkReplyTo(Message message, AddressingProperties maps) {
12241216
}
12251217
}
12261218

1219+
/**
1220+
* Throws a {@code wsa:DestinationUnreachable} SOAP fault when a non-anonymous
1221+
* {@code wsa:ReplyTo} is present but decoupled WS-Addressing is disabled.
1222+
* Must be called before {@link InternalContextUtils#rebaseResponse} so the fault
1223+
* is returned on the synchronous connection, not silently dropped after the 202.
1224+
*/
1225+
private void assertDecoupledReplyToAllowed(Message message, AddressingProperties maps) {
1226+
EndpointReferenceType replyTo = maps.getReplyTo();
1227+
if (ContextUtils.isGenericAddress(replyTo)) {
1228+
return;
1229+
}
1230+
final String uri = replyTo.getAddress().getValue();
1231+
1232+
boolean approved = Boolean.TRUE.equals(
1233+
message.getExchange().get(ContextUtils.DECOUPLED_DESTINATION_APPROVED_PROPERTY));
1234+
if (approved) {
1235+
if (ContextUtils.isDecoupledDestinationSchemeAllowed(uri)) {
1236+
return;
1237+
}
1238+
final String reason = "Decoupled WS-Addressing ReplyTo (" + uri
1239+
+ ") is not permitted by this server: URI scheme is not allowed. "
1240+
+ "Configure permitted schemes with system property "
1241+
+ ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY;
1242+
if (isSOAP12(message)) {
1243+
SoapFault f = new SoapFault(reason, Soap12.getInstance().getSender());
1244+
f.setSubCode(Names.DESTINATION_UNREACHABLE_QNAME);
1245+
throw f;
1246+
}
1247+
throw new SoapFault(reason, Names.DESTINATION_UNREACHABLE_QNAME);
1248+
}
1249+
1250+
if (ContextUtils.isDecoupledDestinationAllowed(uri)) {
1251+
return;
1252+
}
1253+
final String reason = "Decoupled WS-Addressing ReplyTo (" + uri
1254+
+ ") is not permitted by this server. Enable with system property "
1255+
+ ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY + "=true";
1256+
if (isSOAP12(message)) {
1257+
SoapFault f = new SoapFault(reason, Soap12.getInstance().getSender());
1258+
f.setSubCode(Names.DESTINATION_UNREACHABLE_QNAME);
1259+
throw f;
1260+
}
1261+
throw new SoapFault(reason, Names.DESTINATION_UNREACHABLE_QNAME);
1262+
}
1263+
1264+
/**
1265+
* Rebases the response to the non-anonymous {@code wsa:FaultTo} endpoint when
1266+
* decoupled WS-Addressing is permitted, or falls back to the synchronous
1267+
* {@code wsa:ReplyTo} with a warning when it is not.
1268+
*/
1269+
private void rebaseOrFallbackFaultTo(Message message,
1270+
AddressingProperties maps,
1271+
AddressingProperties inMAPs) {
1272+
final String faultToUri = inMAPs.getFaultTo().getAddress().getValue();
1273+
boolean approved = Boolean.TRUE.equals(
1274+
message.getExchange().get(ContextUtils.DECOUPLED_DESTINATION_APPROVED_PROPERTY));
1275+
if (approved) {
1276+
if (ContextUtils.isDecoupledDestinationSchemeAllowed(faultToUri)) {
1277+
Message m = message.getExchange().getInFaultMessage();
1278+
if (m == null) {
1279+
m = message;
1280+
}
1281+
InternalContextUtils.rebaseResponse(inMAPs.getFaultTo(), inMAPs, m);
1282+
Destination destination =
1283+
InternalContextUtils.createDecoupledDestination(m.getExchange(), inMAPs.getFaultTo());
1284+
m.getExchange().setDestination(destination);
1285+
return;
1286+
}
1287+
LOG.log(Level.WARNING,
1288+
"Decoupled pre-approved FaultTo ({0}) is not permitted: URI scheme is not allowed. "
1289+
+ "Fault will be delivered to ReplyTo instead. Configure permitted schemes with {1}",
1290+
new Object[]{faultToUri, ContextUtils.ALLOWED_DECOUPLED_DEST_SCHEMES_PROPERTY});
1291+
if (inMAPs.getReplyTo() != null) {
1292+
maps.setTo(inMAPs.getReplyTo());
1293+
}
1294+
return;
1295+
}
1296+
if (!ContextUtils.isDecoupledDestinationAllowed(faultToUri)) {
1297+
LOG.log(Level.WARNING,
1298+
"Decoupled WS-Addressing FaultTo ({0}) is not permitted; "
1299+
+ "fault will be delivered to ReplyTo instead. "
1300+
+ "Enable with system property {1}=true",
1301+
new Object[]{faultToUri,
1302+
ContextUtils.WS_ADDRESSING_DECOUPLED_ENABLED_PROPERTY});
1303+
if (inMAPs.getReplyTo() != null) {
1304+
maps.setTo(inMAPs.getReplyTo());
1305+
}
1306+
return;
1307+
}
1308+
Message m = message.getExchange().getInFaultMessage();
1309+
if (m == null) {
1310+
m = message;
1311+
}
1312+
InternalContextUtils.rebaseResponse(inMAPs.getFaultTo(), inMAPs, m);
1313+
Destination destination =
1314+
InternalContextUtils.createDecoupledDestination(m.getExchange(), inMAPs.getFaultTo());
1315+
m.getExchange().setDestination(destination);
1316+
}
1317+
12271318
private boolean isSOAP12(Message message) {
12281319
if (message.getExchange().getBinding() instanceof SoapBinding) {
12291320
SoapBinding binding = (SoapBinding)message.getExchange().getBinding();

0 commit comments

Comments
 (0)