CVE-2021-25939 (published 2022-02-09T13:15Z)

In ArangoDB, versions v3.7.0 through v3.9.0-alpha.1 have a feature which allows downloading a Foxx service from a publicly available URL. This feature does not enforce proper filtering of requests performed internally, which can be abused by a highly-privileged attacker to perform blind SSRF and send internal requests to localhost.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:N/I:P/A:N)
CVSS v3 base score: 2.7, LOW (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/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/arangodb/arangodb
commit_details: repo_url: https://github.com/arangodb/arangodb
author: Jan
author_date: 2021-12-14 11:24:01+01:00
msg: [APM-78] Disable installation from remote URL (#15292)
repository: repo_url: https://github.com/arangodb/arangodb
repo_name: arangodb/arangodb
description: 🥑 ArangoDB is a native multi-model database with flexible data models for documents, graphs, and key-values. Build high performance applications using a convenient SQL-like query language or JavaScript extensions.
repo_language: C++
owner: arangodb
file_changes: filename: CHANGELOG
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,10 @@
 devel
 -----
 
+* Added startup security option `--foxx.allow-install-from-remote` to allow 
+  installing Foxx apps from remote URLs other than Github. The option is turned 
+  off by default.
+
 * Cancel ongoing RocksDB compactions on server shutdown.
 
 * Added new option `--custom-query-bindvars` to arangoexport, so queries given 

programming_language: Markdown
filename: ServerSecurityFeature.cpp
change_type: ModificationType.MODIFY
diff: @@ -36,7 +36,8 @@ ServerSecurityFeature::ServerSecurityFeature(application_features::ApplicationSe
     : ApplicationFeature(server, "ServerSecurity"),
       _enableFoxxApi(true),
       _enableFoxxStore(true),
-      _hardenedRestApi(false) {
+      _hardenedRestApi(false),
+      _foxxAllowInstallFromRemote(false) {
   setOptional(false);
   startsAfter<application_features::GreetingsFeaturePhase>();
 }
@@ -55,6 +56,7 @@ void ServerSecurityFeature::collectOptions(std::shared_ptr<ProgramOptions> optio
                      arangodb::options::Flags::OnCoordinator,
                      arangodb::options::Flags::OnSingle))
                      .setIntroducedIn(30500);
+
   options->addOption("--foxx.store", "enables Foxx store in web interface",
                      new BooleanParameter(&_enableFoxxStore),
                      arangodb::options::makeFlags(
@@ -62,7 +64,14 @@ void ServerSecurityFeature::collectOptions(std::shared_ptr<ProgramOptions> optio
                      arangodb::options::Flags::OnCoordinator,
                      arangodb::options::Flags::OnSingle))
                      .setIntroducedIn(30500);
-
+  
+  options->addOption("--foxx.allow-install-from-remote", "allow installing Foxx apps from remote URLs other than Github",
+                     new BooleanParameter(&_foxxAllowInstallFromRemote),
+                     arangodb::options::makeFlags(
+                     arangodb::options::Flags::DefaultNoComponents,
+                     arangodb::options::Flags::OnCoordinator,
+                     arangodb::options::Flags::OnSingle))
+                     .setIntroducedIn(30805);
 }
 
 bool ServerSecurityFeature::isFoxxApiDisabled() const {
@@ -90,3 +99,7 @@ bool ServerSecurityFeature::canAccessHardenedApi() const {
   }
   return allowAccess;
 }
+  
+bool ServerSecurityFeature::foxxAllowInstallFromRemote() const {
+  return _foxxAllowInstallFromRemote;
+}

programming_language: C++
filename: ServerSecurityFeature.h
change_type: ModificationType.MODIFY
diff: @@ -37,11 +37,13 @@ class ServerSecurityFeature final : public application_features::ApplicationFeat
   bool isFoxxApiDisabled() const;
   bool isFoxxStoreDisabled() const;
   bool canAccessHardenedApi() const;
+  bool foxxAllowInstallFromRemote() const;
 
  private:
   bool _enableFoxxApi;
   bool _enableFoxxStore;
   bool _hardenedRestApi;
+  bool _foxxAllowInstallFromRemote;
 };
 
 }  // namespace arangodb

programming_language: Objective-C
filename: v8-actions.cpp
change_type: ModificationType.MODIFY
diff: @@ -1791,6 +1791,17 @@ static void JS_IsFoxxStoreDisabled(v8::FunctionCallbackInfo<v8::Value> const& ar
   TRI_V8_TRY_CATCH_END
 }
 
+static void JS_FoxxAllowInstallFromRemote(v8::FunctionCallbackInfo<v8::Value> const& args) {
+  TRI_V8_TRY_CATCH_BEGIN(isolate)
+  v8::HandleScope scope(isolate);
+
+  TRI_GET_GLOBALS();
+  ServerSecurityFeature& security = v8g->_server.getFeature<ServerSecurityFeature>();
+  TRI_V8_RETURN_BOOL(security.foxxAllowInstallFromRemote());
+
+  TRI_V8_TRY_CATCH_END
+}
+
 static void JS_RunInRestrictedContext(v8::FunctionCallbackInfo<v8::Value> const& args) {
   TRI_V8_TRY_CATCH_BEGIN(isolate)
   v8::HandleScope scope(isolate);
@@ -1868,6 +1879,8 @@ void TRI_InitV8ServerUtils(v8::Isolate* isolate) {
                                TRI_V8_ASCII_STRING(isolate, "SYS_IS_FOXX_API_DISABLED"), JS_IsFoxxApiDisabled, true);
   TRI_AddGlobalFunctionVocbase(isolate,
                                TRI_V8_ASCII_STRING(isolate, "SYS_IS_FOXX_STORE_DISABLED"), JS_IsFoxxStoreDisabled, true);
+  TRI_AddGlobalFunctionVocbase(isolate,
+                               TRI_V8_ASCII_STRING(isolate, "SYS_FOXX_ALLOW_INSTALL_FROM_REMOTE"), JS_FoxxAllowInstallFromRemote, true);
   TRI_AddGlobalFunctionVocbase(isolate,
                                TRI_V8_ASCII_STRING(isolate, "SYS_RUN_IN_RESTRICTED_CONTEXT"), JS_RunInRestrictedContext, true);
   

programming_language: C++
filename: arangod-common.conf
change_type: ModificationType.MODIFY
diff: @@ -9,6 +9,9 @@ role = true
 force-sync-properties = false
 extended-names-databases = true
 
+[foxx]
+allow-install-from-remote = true
+
 [javascript]
 allow-admin-execute = true
 startup-directory = @TOP_DIR@/js

programming_language: Lua
filename: app.js
change_type: ModificationType.MODIFY
diff: @@ -83,6 +83,7 @@ function resolveAppInfo (appInfo, refresh) {
     return {source: `${baseUrl}${splitted[1]}/archive/${splitted[2] || 'master'}.zip`};
   }
   if (/^https?:/i.test(appInfo)) {
+    FoxxManager.validateInstallUrl(appInfo);
     return {source: appInfo};
   }
   if (/^uploads[/\\]tmp-/.test(appInfo)) {

programming_language: TypeScript
filename: aardvark.js
change_type: ModificationType.MODIFY
diff: @@ -90,6 +90,7 @@ router.get('/config.js', function (req, res) {
       statisticsInAllDatabases: internal.enabledStatisticsInAllDatabases(),
       foxxStoreEnabled: !internal.isFoxxStoreDisabled(),
       foxxApiEnabled: !internal.isFoxxApiDisabled(),
+      foxxAllowInstallFromRemote: internal.foxxAllowInstallFromRemote(),
       clusterApiJwtPolicy: internal.clusterApiJwtPolicy(),
       minReplicationFactor: internal.minReplicationFactor,
       maxReplicationFactor: internal.maxReplicationFactor,

programming_language: JavaScript
filename: arango.js
change_type: ModificationType.MODIFY
diff: @@ -581,6 +581,10 @@
       if (!frontendConfig.foxxStoreEnabled) {
         delete menus.Store;
       }
+      
+      if (!frontendConfig.foxxAllowInstallFromRemote) {
+        delete menus.Remote;
+      }
 
       menus[activeKey].active = true;
       if (disabled) {

programming_language: JavaScript
filename: router.js
change_type: ModificationType.MODIFY
diff: @@ -1135,6 +1135,10 @@
           this.navigate('#dashboard', { trigger: true });
           return;
         }
+        if (!frontendConfig.foxxAllowInstallFromRemote) {
+          this.navigate('#services/install/upload', { trigger: true });
+          return;
+        }
         window.modalView.clearValidators();
         if (this.serviceUrlView) {
           this.serviceUrlView.remove();

programming_language: JavaScript
filename: internal.js
change_type: ModificationType.MODIFY
diff: @@ -188,15 +188,20 @@
   // / @brief expose configuration
   // //////////////////////////////////////////////////////////////////////////////
 
-  if (global.SYS_IS_FOXX_API_DISABLED) {
+  if (typeof global.SYS_IS_FOXX_API_DISABLED !== 'undefined') {
     exports.isFoxxApiDisabled = global.SYS_IS_FOXX_API_DISABLED;
     delete global.SYS_IS_FOXX_API_DISABLED;
   }
 
-  if (global.SYS_IS_FOXX_STORE_DISABLED) {
+  if (typeof global.SYS_IS_FOXX_STORE_DISABLED !== 'undefined') {
     exports.isFoxxStoreDisabled = global.SYS_IS_FOXX_STORE_DISABLED;
     delete global.SYS_IS_FOXX_STORE_DISABLED;
   }
+ 
+  if (typeof global.SYS_FOXX_ALLOW_INSTALL_FROM_REMOTE !== 'undefined') {
+    exports.foxxAllowInstallFromRemote = global.SYS_FOXX_ALLOW_INSTALL_FROM_REMOTE;
+    delete global.SYS_FOXX_ALLOW_INSTALL_FROM_REMOTE;
+  }
 
   if (global.SYS_CLUSTER_API_JWT_POLICY) {
     exports.clusterApiJwtPolicy = global.SYS_CLUSTER_API_JWT_POLICY;

programming_language: CoffeeScript
filename: manager.js
change_type: ModificationType.MODIFY
diff: @@ -78,6 +78,33 @@ function isFoxxmaster () {
   return global.ArangoServerState.isFoxxmaster();
 }
 
+function validateInstallUrl (url) {
+  if (!internal.foxxAllowInstallFromRemote()) {
+    // check if a user-defined install baseurl exists
+    let baseUrl = require('process').env.FOXX_BASE_URL;
+    let invalid = false;
+    if (baseUrl) {
+      if (!url.startsWith(baseUrl)) {
+        // install url does not start with FOXX_BASE_URL
+        invalid = true;
+      }
+    } else {
+      const checkRegex = /^https?:\/\/([^:\.]+:[^@\.]*@)?(www\.)?github\.com\//i;
+      invalid = !checkRegex.test(url);
+    }
+
+    if (invalid) {
+      throw new ArangoError({
+        errorNum: errors.ERROR_FORBIDDEN.code,
+        errorMessage: dd`
+          ${errors.ERROR_FORBIDDEN.message}
+          Installing apps from remote URLs is disabled
+        `
+      });
+    }
+  }
+}
+
 // Startup and self-heal
 
 function selfHealAll (skipReloadRouting) {
@@ -562,6 +589,8 @@ function _prepareService (serviceInfo, legacy = false) {
       _buildServiceBundleFromScript(tempServicePath, tempBundlePath, serviceInfo);
     } else if (/^https?:/i.test(serviceInfo)) {
       // Remote path
+      // check if we are allowed to install from this remote URL or not
+      validateInstallUrl(serviceInfo);
       const tempFile = downloadServiceBundleFromRemote(serviceInfo);
       try {
         _buildServiceFromFile(tempServicePath, tempBundlePath, tempFile);
@@ -1112,6 +1141,7 @@ exports.commitLocalState = commitLocalState;
 exports._createServiceBundle = createServiceBundle;
 exports._resetCache = () => GLOBAL_SERVICE_MAP.clear();
 exports._mountPoints = getMountPoints;
+exports.validateInstallUrl = validateInstallUrl;
 
 // -------------------------------------------------
 // Exports from Foxx utils module

programming_language: TypeScript
filename: foxx-allow-install-from-remote-off.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,131 @@
+/*jshint globalstrict:false, strict:false */
+/* global getOptions, assertEqual, assertTrue, assertFalse, arango */
+
+////////////////////////////////////////////////////////////////////////////////
+/// DISCLAIMER
+///
+/// Copyright 2010-2012 triagens GmbH, Cologne, Germany
+///
+/// Licensed 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.
+///
+/// Copyright holder is ArangoDB Inc, Cologne, Germany
+///
+/// @author Jan Steemann
+/// @author Copyright 2021, ArangoDB Inc, Cologne, Germany
+////////////////////////////////////////////////////////////////////////////////
+
+if (getOptions === true) {
+  return {
+    'foxx.allow-install-from-remote': 'false',
+  };
+}
+const jsunity = require('jsunity');
+const errors = require('@arangodb').errors;
+const db = require('internal').db;
+const FoxxManager = require('@arangodb/foxx/manager');
+
+function testSuite() {
+  const mount = "/test123";
+
+  return {
+    testInstallViaAardvarkOk: function() {
+      const urls = [
+        "http://github.com/arangodb-foxx/demo-itzpapalotl/archive/refs/heads/master.zip",
+        "https://github.com/arangodb-foxx/demo-itzpapalotl/archive/refs/heads/master.zip",
+        "http://www.github.com/arangodb-foxx/demo-itzpapalotl/archive/refs/heads/master.zip",
+        "https://www.github.com/arangodb-foxx/demo-itzpapalotl/archive/refs/heads/master.zip",
+      ];
+      urls.forEach((url) => {
+        try {
+          let res = arango.PUT(`/_admin/aardvark/foxxes/url?mount=${mount}`, { url });
+          assertFalse(res.error, url);
+        } finally {
+          try {
+            FoxxManager.uninstall(mount);
+          } catch (err) {}
+        }
+      });
+    },
+
+    testInstallViaAardvarkFail: function() {
+      const urls = [
+        "http://some.other.domain/foo/bar",
+        "https://some.other.domain/foo/bar",
+        "https://github.com.some.deceptive.site/foo/bar",
+        "https://some.deceptive.github.com.site/foo/bar",
+        "https://github.com.evil/foo/bar",
+      ];
+      urls.forEach((url) => {
+        try {
+          let res = arango.PUT(`/_admin/aardvark/foxxes/url?mount=${mount}`, { url });
+          assertTrue(res.error, url);
+          assertEqual(403, res.code);
+        } finally {
+          try {
+            FoxxManager.uninstall(mount);
+          } catch (err) {}
+        }
+      });
+    },
+    
+    testInstallViaFoxxAPIOld: function() {
+      // note: installing from Github is still allowed here
+      const urls = [
+        "http://some.other.domain/foo/bar",
+        "https://some.other.domain/foo/bar",
+        "https://github.com.some.deceptive.site/foo/bar",
+        "https://some.deceptive.github.com.site/foo/bar",
+        "https://github.com.evil/foo/bar",
+      ];
+      urls.forEach((url) => {
+        try {
+          let res = arango.POST("/_admin/foxx/install", { appInfo: url, mount });
+          assertTrue(res.error);
+          assertEqual(403, res.code);
+          assertEqual(11, res.errorNum);
+        } finally {
+          try {
+            FoxxManager.uninstall(mount);
+          } catch (err) {}
+        }
+      });
+    },
+    
+    testInstallViaFoxxAPINew: function() {
+      // note: installing from Github is still allowed here
+      const urls = [
+        "http://some.other.domain/foo/bar",
+        "https://some.other.domain/foo/bar",
+        "https://github.com.some.deceptive.site/foo/bar",
+        "https://some.deceptive.github.com.site/foo/bar",
+        "https://github.com.evil/foo/bar",
+      ];
+      urls.forEach((url) => {
+        try {
+          let res = arango.POST(`/_api/foxx?mount=${mount}`, { source: url });
+          assertTrue(res.error);
+          assertEqual(403, res.code);
+          assertEqual(11, res.errorNum);
+        } finally {
+          try {
+            FoxxManager.uninstall(mount);
+          } catch (err) {}
+        }
+      });
+    },
+    
+  };
+}
+
+jsunity.run(testSuite);
+return jsunity.done();

programming_language: TypeScript
filename: foxx-allow-install-from-remote-on.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,96 @@
+/*jshint globalstrict:false, strict:false */
+/* global getOptions, assertEqual, assertFalse, arango */
+
+////////////////////////////////////////////////////////////////////////////////
+/// DISCLAIMER
+///
+/// Copyright 2010-2012 triagens GmbH, Cologne, Germany
+///
+/// Licensed 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.
+///
+/// Copyright holder is ArangoDB Inc, Cologne, Germany
+///
+/// @author Jan Steemann
+/// @author Copyright 2021, ArangoDB Inc, Cologne, Germany
+////////////////////////////////////////////////////////////////////////////////
+
+if (getOptions === true) {
+  return {
+    'foxx.allow-install-from-remote': 'true',
+  };
+}
+const jsunity = require('jsunity');
+const errors = require('@arangodb').errors;
+const db = require('internal').db;
+const FoxxManager = require('@arangodb/foxx/manager');
+
+function testSuite() {
+  const mount = "/test123";
+
+  return {
+    testInstallViaAardvark: function() {
+      const urls = [
+        "https://github.com/arangodb-foxx/demo-itzpapalotl/archive/refs/heads/master.zip",
+      ];
+      urls.forEach((url) => {
+        try {
+          let res = arango.PUT(`/_admin/aardvark/foxxes/url?mount=${mount}`, { url });
+          assertFalse(res.error);
+          assertEqual("itzpapalotl", res.name);
+        } finally {
+          try {
+            FoxxManager.uninstall(mount);
+          } catch (err) {}
+        }
+      });
+    },
+    
+    testInstallViaFoxxAPIOld: function() {
+      const urls = [
+        "https://github.com/arangodb-foxx/demo-itzpapalotl/archive/refs/heads/master.zip",
+      ];
+      urls.forEach((url) => {
+        try {
+          let res = arango.POST("/_admin/foxx/install", { appInfo: url, mount });
+          assertFalse(res.error);
+          assertEqual("itzpapalotl", res.name);
+        } finally {
+          try {
+            FoxxManager.uninstall(mount);
+          } catch (err) {}
+        }
+      });
+    },
+    
+    testInstallViaFoxxAPINew: function() {
+      const urls = [
+        "https://github.com/arangodb-foxx/demo-itzpapalotl/archive/refs/heads/master.zip",
+      ];
+      urls.forEach((url) => {
+        try {
+          let res = arango.POST(`/_api/foxx?mount=${mount}`, { source: url });
+          assertFalse(res.error);
+          assertEqual("itzpapalotl", res.name);
+        } finally {
+          try {
+            FoxxManager.uninstall(mount);
+          } catch (err) {}
+        }
+      });
+    },
+    
+  };
+}
+
+jsunity.run(testSuite);
+return jsunity.done();

programming_language: JavaScript