CVE-2021-25939 (JavaScript, published 2022-02-09T13:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 2.7)
Weakness: CWE-918 \u2014 Server-Side Request Forgery (SSRF)
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.
Repository: https://github.com/arangodb/arangodb
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 message: [APM-78] Disable installation from remote URL (#15292)



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();