Skip to content

Commit

Permalink
[APM-78] Disable installation from remote URL (#15292) (#15343)
Browse files Browse the repository at this point in the history
* [APM-78] Disable installation from remote URL (#15292)

* Update CHANGELOG

* Fix clang-format

Co-authored-by: Vadim <vadim@arangodb.com>
  • Loading branch information
jsteemann and KVS85 committed Dec 30, 2021
1 parent ca6b418 commit d7b35a6
Show file tree
Hide file tree
Showing 13 changed files with 316 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
@@ -1,6 +1,10 @@
v3.9.0 (XXXX-XX-XX)
-------------------

* APM-78: 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.

* Fixed BTS-693: Sort-limit rule now always ensures proper LIMIT node placement
to avoid possible invalid results in the fullCount data

Expand Down
17 changes: 16 additions & 1 deletion arangod/GeneralServer/ServerSecurityFeature.cpp
Expand Up @@ -37,7 +37,8 @@ ServerSecurityFeature::ServerSecurityFeature(
: ApplicationFeature(server, "ServerSecurity"),
_enableFoxxApi(true),
_enableFoxxStore(true),
_hardenedRestApi(false) {
_hardenedRestApi(false),
_foxxAllowInstallFromRemote(false) {
setOptional(false);
startsAfter<application_features::GreetingsFeaturePhase>();
}
Expand Down Expand Up @@ -68,6 +69,16 @@ void ServerSecurityFeature::collectOptions(
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 {
Expand Down Expand Up @@ -95,3 +106,7 @@ bool ServerSecurityFeature::canAccessHardenedApi() const {
}
return allowAccess;
}

bool ServerSecurityFeature::foxxAllowInstallFromRemote() const {
return _foxxAllowInstallFromRemote;
}
2 changes: 2 additions & 0 deletions arangod/GeneralServer/ServerSecurityFeature.h
Expand Up @@ -39,11 +39,13 @@ class ServerSecurityFeature final
bool isFoxxApiDisabled() const;
bool isFoxxStoreDisabled() const;
bool canAccessHardenedApi() const;
bool foxxAllowInstallFromRemote() const;

private:
bool _enableFoxxApi;
bool _enableFoxxStore;
bool _hardenedRestApi;
bool _foxxAllowInstallFromRemote;
};

} // namespace arangodb
17 changes: 17 additions & 0 deletions arangod/V8Server/v8-actions.cpp
Expand Up @@ -1911,6 +1911,19 @@ static void JS_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)
Expand Down Expand Up @@ -1998,6 +2011,10 @@ void TRI_InitV8ServerUtils(v8::Isolate* isolate) {
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);
Expand Down
3 changes: 3 additions & 0 deletions etc/testing/arangod-common.conf
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions js/actions/_admin/foxx/app.js
Expand Up @@ -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)) {
Expand Down
1 change: 1 addition & 0 deletions js/apps/system/_admin/aardvark/APP/aardvark.js
Expand Up @@ -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,
Expand Down
Expand Up @@ -581,6 +581,10 @@
if (!frontendConfig.foxxStoreEnabled) {
delete menus.Store;
}

if (!frontendConfig.foxxAllowInstallFromRemote) {
delete menus.Remote;
}

menus[activeKey].active = true;
if (disabled) {
Expand Down
Expand Up @@ -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();
Expand Down
9 changes: 7 additions & 2 deletions js/server/bootstrap/modules/internal.js
Expand Up @@ -181,15 +181,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;
Expand Down
30 changes: 30 additions & 0 deletions js/server/modules/@arangodb/foxx/manager.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
@@ -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();

0 comments on commit d7b35a6

Please sign in to comment.