CVE-2021-25939: Server-Side Request Forgery (SSRF) (CWE-918)
value: 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.
CWE 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.
CVSS v3 base score: 2.7
Published: 2022-02-09T13:15Z
Repository: https://github.com/arangodb/arangodb
Language: JavaScript
Files changed: CHANGELOG, arangod/GeneralServer/ServerSecurityFeature.cpp, arangod/GeneralServer/ServerSecurityFeature.h, arangod/V8Server/v8-actions.cpp, etc/testing/arangod-common.conf, js/actions/_admin/foxx/app.js, js/apps/system/_admin/aardvark/APP/aardvark.js, js/apps/system/_admin/aardvark/APP/frontend/js/arango/arango.js, js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js, js/server/bootstrap/modules/internal.js, js/server/modules/@arangodb/foxx/manager.js, tests/js/client/server_parameters/foxx-allow-install-from-remote-off.js, tests/js/client/server_parameters/foxx-allow-install-from-remote-on.js
Commit date: 2021-12-14 11:24:01 +0100
Commit message: [APM-78] Disable installation from remote URL (#15292)


Diff: diff --git a/CHANGELOG b/CHANGELOG
index 4e80994a77..52a7e6f6c1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,8 +1,12 @@
 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 
   via option `--custom-query` can have bind variables in them. Also changed the 
   flag names `--query` to `--custom-query` and `--query-max-runtime` to 
diff --git a/arangod/GeneralServer/ServerSecurityFeature.cpp b/arangod/GeneralServer/ServerSecurityFeature.cpp
index 7c887c5c51..b3da8281b3 100644
--- a/arangod/GeneralServer/ServerSecurityFeature.cpp
+++ b/arangod/GeneralServer/ServerSecurityFeature.cpp
@@ -34,11 +34,12 @@ using namespace arangodb::options;
 
 ServerSecurityFeature::ServerSecurityFeature(application_features::ApplicationServer& server)
     : ApplicationFeature(server, "ServerSecurity"),
       _enableFoxxApi(true),
       _enableFoxxStore(true),
-      _hardenedRestApi(false) {
+      _hardenedRestApi(false),
+      _foxxAllowInstallFromRemote(false) {
   setOptional(false);
   startsAfter<application_features::GreetingsFeaturePhase>();
 }
 
 void ServerSecurityFeature::collectOptions(std::shared_ptr<ProgramOptions> options) {
@@ -53,18 +54,26 @@ void ServerSecurityFeature::collectOptions(std::shared_ptr<ProgramOptions> optio
                      arangodb::options::makeFlags(
                      arangodb::options::Flags::DefaultNoComponents,
                      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(
                      arangodb::options::Flags::DefaultNoComponents,
                      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 {
   return !_enableFoxxApi;
 }
@@ -88,5 +97,9 @@ bool ServerSecurityFeature::canAccessHardenedApi() const {
       allowAccess = true;
     }
   }
   return allowAccess;
 }
+  
+bool ServerSecurityFeature::foxxAllowInstallFromRemote() const {
+  return _foxxAllowInstallFromRemote;
+}
diff --git a/arangod/GeneralServer/ServerSecurityFeature.h b/arangod/GeneralServer/ServerSecurityFeature.h
index e7f34bdfe4..eded174fbc 100644
--- a/arangod/GeneralServer/ServerSecurityFeature.h
+++ b/arangod/GeneralServer/ServerSecurityFeature.h
@@ -35,14 +35,16 @@ class ServerSecurityFeature final : public application_features::ApplicationFeat
 
   bool isRestApiHardened() const;
   bool isFoxxApiDisabled() const;
   bool isFoxxStoreDisabled() const;
   bool canAccessHardenedApi() const;
+  bool foxxAllowInstallFromRemote() const;
 
  private:
   bool _enableFoxxApi;
   bool _enableFoxxStore;
   bool _hardenedRestApi;
+  bool _foxxAllowInstallFromRemote;
 };
 
 }  // namespace arangodb
 
diff --git a/arangod/V8Server/v8-actions.cpp b/arangod/V8Server/v8-actions.cpp
index ab610688ed..f6b01f0a26 100644
--- a/arangod/V8Server/v8-actions.cpp
+++ b/arangod/V8Server/v8-actions.cpp
@@ -1789,10 +1789,21 @@ static void JS_IsFoxxStoreDisabled(v8::FunctionCallbackInfo<v8::Value> const& ar
   TRI_V8_RETURN_BOOL(security.isFoxxStoreDisabled());
 
   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);
 
   if (args.Length() != 1 || !args[0]->IsFunction()) {
@@ -1866,10 +1877,12 @@ void TRI_InitV8ServerUtils(v8::Isolate* isolate) {
                                TRI_V8_ASCII_STRING(isolate, "SYS_CLUSTER_API_JWT_POLICY"), JS_ClusterApiJwtPolicy, true);
   TRI_AddGlobalFunctionVocbase(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);
   
   TRI_AddGlobalFunctionVocbase(isolate,
                                TRI_V8_ASCII_STRING(isolate,
diff --git a/etc/testing/arangod-common.conf b/etc/testing/arangod-common.conf
index ee9b010538..390e5b8c4e 100644
--- a/etc/testing/arangod-common.conf
+++ b/etc/testing/arangod-common.conf
@@ -7,10 +7,13 @@ role = true
 
 [database]
 force-sync-properties = false
 extended-names-databases = true
 
+[foxx]
+allow-install-from-remote = true
+
 [javascript]
 allow-admin-execute = true
 startup-directory = @TOP_DIR@/js
 module-directory = @TOP_DIR@/enterprise/js
 
diff --git a/js/actions/_admin/foxx/app.js b/js/actions/_admin/foxx/app.js
index ec6cd2f2a4..249dd8cbbb 100644
--- a/js/actions/_admin/foxx/app.js
+++ b/js/actions/_admin/foxx/app.js
@@ -81,10 +81,11 @@ function resolveAppInfo (appInfo, refresh) {
     const splitted = appInfo.split(':');
     const baseUrl = process.env.FOXX_BASE_URL || 'https://github.com/';
     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)) {
     const tempFile = joinPath(fs.getTempPath(), appInfo);
     const buffer = fs.readFileSync(tempFile);
diff --git a/js/apps/system/_admin/aardvark/APP/aardvark.js b/js/apps/system/_admin/aardvark/APP/aardvark.js
index e1bd7316bf..c9c92a237d 100644
--- a/js/apps/system/_admin/aardvark/APP/aardvark.js
+++ b/js/apps/system/_admin/aardvark/APP/aardvark.js
@@ -88,10 +88,11 @@ router.get('/config.js', function (req, res) {
       statisticsEnabled: internal.enabledStatistics(),
       metricsEnabled: internal.enabledMetrics(),
       statisticsInAllDatabases: internal.enabledStatisticsInAllDatabases(),
       foxxStoreEnabled: !internal.isFoxxStoreDisabled(),
       foxxApiEnabled: !internal.isFoxxApiDisabled(),
+      foxxAllowInstallFromRemote: internal.foxxAllowInstallFromRemote(),
       clusterApiJwtPolicy: internal.clusterApiJwtPolicy(),
       minReplicationFactor: internal.minReplicationFactor,
       maxReplicationFactor: internal.maxReplicationFactor,
       defaultReplicationFactor: internal.defaultReplicationFactor,
       maxNumberOfShards: internal.maxNumberOfShards,
diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/arango/arango.js b/js/apps/system/_admin/aardvark/APP/frontend/js/arango/arango.js
index aec771ed0f..3536f9ebf8 100644
--- a/js/apps/system/_admin/aardvark/APP/frontend/js/arango/arango.js
+++ b/js/apps/system/_admin/aardvark/APP/frontend/js/arango/arango.js
@@ -579,10 +579,14 @@
       };
 
       if (!frontendConfig.foxxStoreEnabled) {
         delete menus.Store;
       }
+      
+      if (!frontendConfig.foxxAllowInstallFromRemote) {
+        delete menus.Remote;
+      }
 
       menus[activeKey].active = true;
       if (disabled) {
         menus[disabled].disabled = true;
       }
diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js b/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js
index 596d207315..0aef7a01cc 100644
--- a/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js
+++ b/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js
@@ -1133,10 +1133,14 @@
       this.init.then(() => {
         if (!this.foxxApiEnabled) {
           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();
         }
         this.serviceUrlView = new window.ServiceInstallUrlView({
diff --git a/js/server/bootstrap/modules/internal.js b/js/server/bootstrap/modules/internal.js
index 5e4da543ab..3cb6c61557 100644
--- a/js/server/bootstrap/modules/internal.js
+++ b/js/server/bootstrap/modules/internal.js
@@ -186,19 +186,24 @@
 
   // //////////////////////////////////////////////////////////////////////////////
   // / @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;
     delete global.SYS_CLUSTER_API_JWT_POLICY;
   }
diff --git a/js/server/modules/@arangodb/foxx/manager.js b/js/server/modules/@arangodb/foxx/manager.js
index 44b242c04a..4bf81e2a8a 100644
--- a/js/server/modules/@arangodb/foxx/manager.js
+++ b/js/server/modules/@arangodb/foxx/manager.js
@@ -76,10 +76,37 @@ function getMyCoordinatorId () {
 
 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) {
   const db = require('internal').db;
   const dbName = db._name();
@@ -560,10 +587,12 @@ function _prepareService (serviceInfo, legacy = false) {
     } else if (serviceInfo instanceof Buffer) {
       // Buffer (js)
       _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);
       } finally {
         fs.remove(tempFile);
@@ -1110,10 +1139,11 @@ exports.heal = triggerSelfHeal;
 exports.healAll = selfHealAll;
 exports.commitLocalState = commitLocalState;
 exports._createServiceBundle = createServiceBundle;
 exports._resetCache = () => GLOBAL_SERVICE_MAP.clear();
 exports._mountPoints = getMountPoints;
+exports.validateInstallUrl = validateInstallUrl;
 
 // -------------------------------------------------
 // Exports from Foxx utils module
 // -------------------------------------------------
 
diff --git a/tests/js/client/server_parameters/foxx-allow-install-from-remote-off.js b/tests/js/client/server_parameters/foxx-allow-install-from-remote-off.js
new file mode 100644
index 0000000000..f4e18ee724
--- /dev/null
+++ b/tests/js/client/server_parameters/foxx-allow-install-from-remote-off.js
@@ -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();
diff --git a/tests/js/client/server_parameters/foxx-allow-install-from-remote-on.js b/tests/js/client/server_parameters/foxx-allow-install-from-remote-on.js
new file mode 100644
index 0000000000..5d68b4cc35
--- /dev/null
+++ b/tests/js/client/server_parameters/foxx-allow-install-from-remote-on.js
@@ -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();

Vulnerable code:       _hardenedRestApi(false) {

  if (global.SYS_IS_FOXX_API_DISABLED) {
  if (global.SYS_IS_FOXX_STORE_DISABLED) {
Fixed code: * 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.

      _hardenedRestApi(false),
      _foxxAllowInstallFromRemote(false) {

  
  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::foxxAllowInstallFromRemote() const {
  return _foxxAllowInstallFromRemote;
}
  bool foxxAllowInstallFromRemote() const;
  bool _foxxAllowInstallFromRemote;
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
}

  TRI_AddGlobalFunctionVocbase(isolate,
                               TRI_V8_ASCII_STRING(isolate, "SYS_FOXX_ALLOW_INSTALL_FROM_REMOTE"), JS_FoxxAllowInstallFromRemote, true);
[foxx]
allow-install-from-remote = true

    FoxxManager.validateInstallUrl(appInfo);
      foxxAllowInstallFromRemote: internal.foxxAllowInstallFromRemote(),
      
      if (!frontendConfig.foxxAllowInstallFromRemote) {
        delete menus.Remote;
      }
        if (!frontendConfig.foxxAllowInstallFromRemote) {
          this.navigate('#services/install/upload', { trigger: true });
          return;
        }
  if (typeof global.SYS_IS_FOXX_API_DISABLED !== 'undefined') {
  if (typeof global.SYS_IS_FOXX_STORE_DISABLED !== 'undefined') {
 
  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;
  }
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
        `
      });
    }
  }
}

      // check if we are allowed to install from this remote URL or not
      validateInstallUrl(serviceInfo);
exports.validateInstallUrl = validateInstallUrl;
/*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();
/*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();