CVE-2022-4096 (published 2022-11-21T15:15Z)

Server-Side Request Forgery (SSRF) in GitHub repository appsmithorg/appsmith prior to 1.8.2.

CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/appsmithorg/appsmith
commit_details: repo_url: https://github.com/appsmithorg/appsmith
author: Shrikant Sharat Kandula
author_date: 2022-09-19 12:41:57+05:30
msg: fix: Better support for disallowed hosts (#16842)
repository: repo_url: https://github.com/appsmithorg/appsmith
repo_name: appsmithorg/appsmith
description: Platform to build admin panels, internal tools, and dashboards. Integrates with 25+ databases and any API.
repo_language: TypeScript
owner: appsmithorg
file_changes: filename: TriggerUtils.java
change_type: ModificationType.MODIFY
diff: @@ -35,10 +35,8 @@ import reactor.netty.resources.ConnectionProvider;
 
 import javax.crypto.SecretKey;
 import java.io.IOException;
-import java.net.InetAddress;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.time.Instant;
@@ -46,7 +44,6 @@ import java.util.Date;
 import java.util.List;
 import java.util.Set;
 
-import static com.appsmith.external.helpers.restApiUtils.helpers.URIUtils.DISALLOWED_HOSTS;
 import static org.apache.commons.lang3.StringUtils.isNotEmpty;
 
 @NoArgsConstructor
@@ -204,14 +201,10 @@ public class TriggerUtils {
                          * It redirects to partial URI : /api/character/
                          * In this scenario we should convert the partial URI to complete URI
                          */
-                        URI redirectUri;
+                        final URI redirectUri;
                         try {
                             redirectUri = new URI(redirectUrl);
-                            if (DISALLOWED_HOSTS.contains(redirectUri.getHost())
-                                    || DISALLOWED_HOSTS.contains(InetAddress.getByName(redirectUri.getHost()).getHostAddress())) {
-                                return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Host not allowed."));
-                            }
-                        } catch (URISyntaxException | UnknownHostException e) {
+                        } catch (URISyntaxException e) {
                             return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e));
                         }
 

programming_language: Java
filename: URIUtils.java
change_type: ModificationType.MODIFY
diff: @@ -3,30 +3,21 @@ package com.appsmith.external.helpers.restApiUtils.helpers;
 import com.appsmith.external.models.ActionConfiguration;
 import com.appsmith.external.models.DatasourceConfiguration;
 import com.appsmith.external.models.Property;
-import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
-import org.apache.commons.lang3.StringUtils;
 import org.springframework.web.util.UriComponentsBuilder;
 
-import java.net.InetAddress;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLEncoder;
-import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Set;
 
 import static org.apache.commons.collections.CollectionUtils.isEmpty;
 import static org.apache.commons.lang3.StringUtils.isNotEmpty;
 
 @NoArgsConstructor
 public class URIUtils {
-    public static final Set<String> DISALLOWED_HOSTS = Set.of(
-            "169.254.169.254",
-            "metadata.google.internal"
-    );
 
     public URI createUriWithQueryParams(ActionConfiguration actionConfiguration,
                                         DatasourceConfiguration datasourceConfiguration, String url,
@@ -53,7 +44,7 @@ public class URIUtils {
             for (Property queryParam : allQueryParams) {
                 String key = queryParam.getKey();
                 if (isNotEmpty(key)) {
-                    if (encodeParamsToggle == true) {
+                    if (encodeParamsToggle) {
                         uriBuilder.queryParam(
                                 URLEncoder.encode(key, StandardCharsets.UTF_8),
                                 URLEncoder.encode((String) queryParam.getValue(), StandardCharsets.UTF_8)
@@ -78,9 +69,4 @@ public class URIUtils {
         return "http://" + url;
     }
 
-    public boolean isHostDisallowed(URI uri) throws UnknownHostException {
-        String host = uri.getHost();
-        return StringUtils.isEmpty(host) || DISALLOWED_HOSTS.contains(host)
-                || DISALLOWED_HOSTS.contains(InetAddress.getByName(host).getHostAddress());
-    }
 }

programming_language: Java
filename: WebClientUtils.java
change_type: ModificationType.MODIFY
diff: @@ -1,11 +1,31 @@
 package com.appsmith.util;
 
+import io.netty.resolver.AddressResolver;
+import io.netty.resolver.AddressResolverGroup;
+import io.netty.resolver.InetNameResolver;
+import io.netty.resolver.InetSocketAddressResolver;
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Promise;
+import io.netty.util.internal.SocketUtils;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.client.reactive.ReactorClientHttpConnector;
 import org.springframework.web.reactive.function.client.WebClient;
 import reactor.netty.http.client.HttpClient;
 
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
 public class WebClientUtils {
 
+    private static final Set<String> DISALLOWED_HOSTS = Set.of(
+            "169.254.169.254",
+            "metadata.google.internal"
+    );
+
     private WebClientUtils() {
     }
 
@@ -31,15 +51,86 @@ public class WebClientUtils {
 
     public static WebClient.Builder builder(HttpClient httpClient) {
         return WebClient.builder()
-                .clientConnector(new ReactorClientHttpConnector(applyProxyIfConfigured(httpClient)));
+                .clientConnector(new ReactorClientHttpConnector(makeSafeHttpClient(httpClient)));
     }
 
-    private static HttpClient applyProxyIfConfigured(HttpClient httpClient) {
+    private static HttpClient makeSafeHttpClient(HttpClient httpClient) {
         if (shouldUseSystemProxy()) {
             httpClient = httpClient.proxyWithSystemProperties();
         }
 
-        return httpClient;
+        return httpClient.resolver(ResolverGroup.INSTANCE);
+    }
+
+    private static class ResolverGroup extends AddressResolverGroup<InetSocketAddress> {
+        public static final ResolverGroup INSTANCE = new ResolverGroup();
+
+        @Override
+        protected AddressResolver<InetSocketAddress> newResolver(EventExecutor executor) {
+            return new InetSocketAddressResolver(executor, new NameResolver(executor));
+        }
+    }
+
+    @Slf4j
+    private static class NameResolver extends InetNameResolver {
+
+        public NameResolver(EventExecutor executor) {
+            super(executor);
+        }
+
+        private static boolean isDisallowedAndFail(String host, Promise<?> promise) {
+            if (DISALLOWED_HOSTS.contains(host)) {
+                log.warn("Host {} is disallowed. Failing the request.", host);
+                promise.setFailure(new UnknownHostException("Host not allowed."));
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        protected void doResolve(String inetHost, Promise<InetAddress> promise) {
+            if (isDisallowedAndFail(inetHost, promise)) {
+                return;
+            }
+
+            final InetAddress address;
+            try {
+                address = SocketUtils.addressByName(inetHost);
+            } catch (UnknownHostException e) {
+                promise.setFailure(e);
+                return;
+            }
+
+            if (isDisallowedAndFail(address.getHostAddress(), promise)) {
+                return;
+            }
+
+            promise.setSuccess(address);
+        }
+
+        @Override
+        protected void doResolveAll(String inetHost, Promise<List<InetAddress>> promise) {
+            if (isDisallowedAndFail(inetHost, promise)) {
+                return;
+            }
+
+            final List<InetAddress> addresses;
+            try {
+                addresses = Arrays.asList(SocketUtils.allAddressesByName(inetHost));
+            } catch (UnknownHostException e) {
+                promise.setFailure(e);
+                return;
+            }
+
+            // Even if _one_ of the addresses is disallowed, we fail the request.
+            for (InetAddress address : addresses) {
+                if (isDisallowedAndFail(address.getHostAddress(), promise)) {
+                    return;
+                }
+            }
+
+            promise.setSuccess(addresses);
+        }
     }
 
 }

programming_language: Java
filename: GraphQLPlugin.java
change_type: ModificationType.MODIFY
diff: @@ -27,7 +27,6 @@ import reactor.core.publisher.Mono;
 
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -37,12 +36,12 @@ import java.util.Set;
 import static com.appsmith.external.helpers.PluginUtils.getValueSafelyFromPropertyList;
 import static com.appsmith.external.helpers.PluginUtils.setValueSafelyInPropertyList;
 import static com.external.utils.GraphQLBodyUtils.PAGINATION_DATA_INDEX;
-import static com.external.utils.GraphQLDataTypeUtils.smartlyReplaceGraphQLQueryBodyPlaceholderWithValue;
-import static com.external.utils.GraphQLPaginationUtils.updateVariablesWithPaginationValues;
 import static com.external.utils.GraphQLBodyUtils.QUERY_VARIABLES_INDEX;
 import static com.external.utils.GraphQLBodyUtils.convertToGraphQLPOSTBodyFormat;
 import static com.external.utils.GraphQLBodyUtils.getGraphQLQueryParamsForBodyAndVariables;
 import static com.external.utils.GraphQLBodyUtils.validateBodyAndVariablesSyntax;
+import static com.external.utils.GraphQLDataTypeUtils.smartlyReplaceGraphQLQueryBodyPlaceholderWithValue;
+import static com.external.utils.GraphQLPaginationUtils.updateVariablesWithPaginationValues;
 import static java.lang.Boolean.TRUE;
 import static org.apache.commons.lang3.StringUtils.isBlank;
 
@@ -186,18 +185,6 @@ public class GraphQLPlugin extends BasePlugin {
             ActionExecutionRequest actionExecutionRequest =
                     RequestCaptureFilter.populateRequestFields(actionConfiguration, uri, insertedParams, objectMapper);
 
-            try {
-                if (uriUtils.isHostDisallowed(uri)) {
-                    errorResult.setBody(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR.getMessage("Host not allowed."));
-                    errorResult.setRequest(actionExecutionRequest);
-                    return Mono.just(errorResult);
-                }
-            } catch (UnknownHostException e) {
-                errorResult.setBody(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR.getMessage("Unknown host."));
-                errorResult.setRequest(actionExecutionRequest);
-                return Mono.just(errorResult);
-            }
-
             WebClient.Builder webClientBuilder = triggerUtils.getWebClientBuilder(actionConfiguration,
                     datasourceConfiguration);
 
@@ -282,7 +269,7 @@ public class GraphQLPlugin extends BasePlugin {
                     EXCHANGE_STRATEGIES, requestCaptureFilter);
 
             /* Triggering the actual REST API call */
-            Set<String> hintMessages = new HashSet<String>();
+            Set<String> hintMessages = new HashSet<>();
             return triggerUtils.triggerApiCall(client, httpMethod, uri, requestBodyObj, actionExecutionRequest,
                     objectMapper,
                     hintMessages, errorResult, requestCaptureFilter);

programming_language: Java
filename: RestApiPlugin.java
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,8 @@ import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
 import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
 import com.appsmith.external.helpers.DataTypeStringUtils;
 import com.appsmith.external.helpers.MustacheHelper;
+import com.appsmith.external.helpers.restApiUtils.connections.APIConnection;
+import com.appsmith.external.helpers.restApiUtils.helpers.RequestCaptureFilter;
 import com.appsmith.external.models.ActionConfiguration;
 import com.appsmith.external.models.ActionExecutionRequest;
 import com.appsmith.external.models.ActionExecutionResult;
@@ -15,18 +17,16 @@ import com.appsmith.external.models.Property;
 import com.appsmith.external.plugins.BasePlugin;
 import com.appsmith.external.plugins.BaseRestApiPluginExecutor;
 import com.appsmith.external.services.SharedConfig;
-import com.appsmith.external.helpers.restApiUtils.connections.APIConnection;
-import com.appsmith.external.helpers.restApiUtils.helpers.RequestCaptureFilter;
 import lombok.extern.slf4j.Slf4j;
 import org.pf4j.Extension;
 import org.pf4j.PluginWrapper;
 import org.springframework.http.HttpMethod;
 import org.springframework.web.reactive.function.client.WebClient;
 import reactor.core.publisher.Mono;
+
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLDecoder;
-import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -125,7 +125,7 @@ public class RestApiPlugin extends BasePlugin {
             initUtils.initializeResponseWithError(errorResult);
 
             // Set of hint messages that can be returned to the user.
-            Set<String> hintMessages = new HashSet();
+            Set<String> hintMessages = new HashSet<>();
 
             // Initializing request URL
             String url = initUtils.initializeRequestUrl(actionConfiguration, datasourceConfiguration);
@@ -148,18 +148,6 @@ public class RestApiPlugin extends BasePlugin {
             ActionExecutionRequest actionExecutionRequest =
                     RequestCaptureFilter.populateRequestFields(actionConfiguration, uri, insertedParams, objectMapper);
 
-            try {
-                if (uriUtils.isHostDisallowed(uri)) {
-                    errorResult.setBody(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR.getMessage("Host not allowed."));
-                    errorResult.setRequest(actionExecutionRequest);
-                    return Mono.just(errorResult);
-                }
-            } catch (UnknownHostException e) {
-                errorResult.setBody(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR.getMessage("Unknown host."));
-                errorResult.setRequest(actionExecutionRequest);
-                return Mono.just(errorResult);
-            }
-
             WebClient.Builder webClientBuilder = triggerUtils.getWebClientBuilder(actionConfiguration,
                     datasourceConfiguration);
             String reqContentType = headerUtils.getRequestContentType(actionConfiguration, datasourceConfiguration);

programming_language: Java