Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 71 additions & 9 deletions core/src/main/java/org/apache/cxf/ws/addressing/ContextUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <em>disabled</em> 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.
*
* <p>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://}).
*
* <p>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
Expand Down Expand Up @@ -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 <em>WS-Addressing</em> path.
*
* <p>WS-Addressing decoupled destinations are <em>disabled by default</em> 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}.
*
* <p>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;
}
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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();
Expand Down
Loading
Loading