1
0
Fork 0
arangodb/lib/ApplicationFeatures/V8SecurityFeature.cpp

443 lines
16 KiB
C++

////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2016 ArangoDB 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 GmbH, Cologne, Germany
///
/// @author Jan Steemann
////////////////////////////////////////////////////////////////////////////////
#include <stdlib.h>
#include <algorithm>
#include <cstdint>
#include <exception>
#include <iterator>
#include <map>
#include <sstream>
#include <stdexcept>
#include <utility>
#include "ApplicationFeatures/V8SecurityFeature.h"
#include "ApplicationFeatures/ApplicationServer.h"
#include "ApplicationFeatures/TempFeature.h"
#include "ApplicationFeatures/V8PlatformFeature.h"
#include "Basics/FileResultString.h"
#include "Basics/FileUtils.h"
#include "Basics/StringUtils.h"
#include "Basics/application-exit.h"
#include "Basics/debugging.h"
#include "Basics/files.h"
#include "Basics/operating-system.h"
#include "Logger/LogMacros.h"
#include "Logger/Logger.h"
#include "Logger/LoggerStream.h"
#include "ProgramOptions/Option.h"
#include "ProgramOptions/Parameters.h"
#include "ProgramOptions/ProgramOptions.h"
#include "V8/JavaScriptSecurityContext.h"
#include "V8/v8-globals.h"
using namespace arangodb;
using namespace arangodb::basics;
using namespace arangodb::options;
namespace {
void testRegexPair(std::string const& whitelist, std::string const& blacklist,
char const* optionName) {
try {
std::regex(whitelist, std::regex::nosubs | std::regex::ECMAScript);
} catch (std::exception const& ex) {
LOG_TOPIC("ab6d5", FATAL, arangodb::Logger::FIXME)
<< "value for '--javascript." << optionName
<< "-whitelist' is not a "
"valid regular expression: "
<< ex.what();
FATAL_ERROR_EXIT();
}
try {
std::regex(blacklist, std::regex::nosubs | std::regex::ECMAScript);
} catch (std::exception const& ex) {
LOG_TOPIC("ab2d5", FATAL, arangodb::Logger::FIXME)
<< "value for '--javascript." << optionName
<< "-blacklist' is not a "
"valid regular expression: "
<< ex.what();
FATAL_ERROR_EXIT();
}
}
std::string canonicalpath(std::string const& path) {
#ifndef _WIN32
auto realPath =
std::unique_ptr<char, void (*)(void*)>(::realpath(path.c_str(), nullptr), &free);
if (realPath) {
return std::string(realPath.get());
}
// fallthrough intentional
#endif
return path;
}
void convertToSingleExpression(std::vector<std::string> const& values, std::string& targetRegex) {
if (values.empty()) {
return;
}
targetRegex = "(" + arangodb::basics::StringUtils::join(values, '|') + ")";
}
void convertToSingleExpression(std::unordered_set<std::string> const& values,
std::string& targetRegex) {
// does not delete from the set
if (values.empty()) {
return;
}
auto last = *values.cbegin();
std::stringstream ss;
ss << "(";
for (auto it = std::next(values.cbegin()); it != values.cend(); ++it) {
ss << *it << "|";
}
ss << last;
ss << ")";
targetRegex = ss.str();
}
bool checkBlackAndWhitelist(std::string const& value, bool hasWhitelist,
std::regex const& whitelist, bool hasBlacklist,
std::regex const& blacklist) {
if (!hasWhitelist && !hasBlacklist) {
return true;
}
if (!hasBlacklist) {
// only have a whitelist
return std::regex_search(value, whitelist);
}
if (!hasWhitelist) {
// only have a blacklist
return !std::regex_search(value, blacklist);
}
std::smatch white_result{};
std::smatch black_result{};
bool white = std::regex_search(value, white_result, whitelist);
bool black = std::regex_search(value, black_result, blacklist);
if (white && !black) {
// we only have a whitelist hit => allow
return true;
} else if (!white && black) {
// we only have a blacklist hit => deny
return false;
} else if (!white && !black) {
// we have neither a whitelist nor a blacklist hit => deny
return false;
}
// longer match or blacklist wins
return white_result[0].length() > black_result[0].length();
}
} // namespace
V8SecurityFeature::V8SecurityFeature(application_features::ApplicationServer& server)
: ApplicationFeature(server, "V8Security"),
_hardenInternalModule(false),
_allowProcessControl(false),
_allowPortTesting(false) {
setOptional(false);
startsAfter<TempFeature>();
startsAfter<V8PlatformFeature>();
}
void V8SecurityFeature::collectOptions(std::shared_ptr<ProgramOptions> options) {
options->addSection("javascript", "Configure the Javascript engine");
options
->addOption("--javascript.allow-port-testing",
"allow testing of ports from within JavaScript actions",
new BooleanParameter(&_allowPortTesting),
arangodb::options::makeFlags(arangodb::options::Flags::Hidden))
.setIntroducedIn(30500);
options
->addOption("--javascript.allow-external-process-control",
"allow execution and control of external processes from "
"within JavaScript actions",
new BooleanParameter(&_allowProcessControl),
arangodb::options::makeFlags(arangodb::options::Flags::Hidden))
.setIntroducedIn(30500);
options
->addOption("--javascript.harden",
"disables access to JavaScript functions in the internal "
"module: getPid() and logLevel()",
new BooleanParameter(&_hardenInternalModule))
.setIntroducedIn(30500);
options
->addOption("--javascript.startup-options-whitelist",
"startup options whose names match this regular "
"expression will be whitelisted and exposed to JavaScript",
new VectorParameter<StringParameter>(&_startupOptionsWhitelistVec))
.setIntroducedIn(30500);
options
->addOption("--javascript.startup-options-blacklist",
"startup options whose names match this regular "
"expression will not be exposed (if not whitelisted) to "
"JavaScript actions",
new VectorParameter<StringParameter>(&_startupOptionsBlacklistVec))
.setIntroducedIn(30500);
options
->addOption("--javascript.environment-variables-whitelist",
"environment variables that will be accessible in JavaScript",
new VectorParameter<StringParameter>(&_environmentVariablesWhitelistVec))
.setIntroducedIn(30500);
options
->addOption("--javascript.environment-variables-blacklist",
"environment variables that will be inaccessible in "
"JavaScript if not whitelisted",
new VectorParameter<StringParameter>(&_environmentVariablesBlacklistVec))
.setIntroducedIn(30500);
options
->addOption("--javascript.endpoints-whitelist",
"endpoints that can be connected to via "
"@arangodb/request module in JavaScript actions",
new VectorParameter<StringParameter>(&_endpointsWhitelistVec))
.setIntroducedIn(30500);
options
->addOption("--javascript.endpoints-blacklist",
"endpoints that cannot be connected to via @arangodb/request "
"module in "
"JavaScript actions if not whitelisted",
new VectorParameter<StringParameter>(&_endpointsBlacklistVec))
.setIntroducedIn(30500);
options
->addOption("--javascript.files-whitelist",
"filesystem paths that will be accessible from within "
"JavaScript actions",
new VectorParameter<StringParameter>(&_filesWhitelistVec))
.setIntroducedIn(30500);
}
void V8SecurityFeature::validateOptions(std::shared_ptr<ProgramOptions> options) {
// check if the regular expressions compile properly
// startup options
convertToSingleExpression(_startupOptionsWhitelistVec, _startupOptionsWhitelist);
convertToSingleExpression(_startupOptionsBlacklistVec, _startupOptionsBlacklist);
testRegexPair(_startupOptionsWhitelist, _startupOptionsBlacklist,
"startup-options");
// environment variables
convertToSingleExpression(_environmentVariablesWhitelistVec, _environmentVariablesWhitelist);
convertToSingleExpression(_environmentVariablesBlacklistVec, _environmentVariablesBlacklist);
testRegexPair(_environmentVariablesWhitelist, _environmentVariablesBlacklist,
"environment-variables");
// endpoints
convertToSingleExpression(_endpointsWhitelistVec, _endpointsWhitelist);
convertToSingleExpression(_endpointsBlacklistVec, _endpointsBlacklist);
testRegexPair(_endpointsWhitelist, _endpointsBlacklist, "endpoints");
// file access
convertToSingleExpression(_filesWhitelistVec, _filesWhitelist);
testRegexPair(_filesWhitelist, "", "files");
}
void V8SecurityFeature::prepare() {
addToInternalWhitelist(TRI_GetTempPath(), FSAccessType::READ);
addToInternalWhitelist(TRI_GetTempPath(), FSAccessType::WRITE);
TRI_ASSERT(!_writeWhitelist.empty());
TRI_ASSERT(!_readWhitelist.empty());
}
void V8SecurityFeature::start() {
// initialize regexes for filtering options. the regexes must have been validated before
_startupOptionsWhitelistRegex =
std::regex(_startupOptionsWhitelist, std::regex::nosubs | std::regex::ECMAScript);
_startupOptionsBlacklistRegex =
std::regex(_startupOptionsBlacklist, std::regex::nosubs | std::regex::ECMAScript);
_environmentVariablesWhitelistRegex =
std::regex(_environmentVariablesWhitelist, std::regex::nosubs | std::regex::ECMAScript);
_environmentVariablesBlacklistRegex =
std::regex(_environmentVariablesBlacklist, std::regex::nosubs | std::regex::ECMAScript);
_endpointsWhitelistRegex =
std::regex(_endpointsWhitelist, std::regex::nosubs | std::regex::ECMAScript);
_endpointsBlacklistRegex =
std::regex(_endpointsBlacklist, std::regex::nosubs | std::regex::ECMAScript);
_filesWhitelistRegex =
std::regex(_filesWhitelist, std::regex::nosubs | std::regex::ECMAScript);
}
void V8SecurityFeature::dumpAccessLists() const {
LOG_TOPIC("2cafe", DEBUG, arangodb::Logger::SECURITY)
<< "files whitelisted by user:" << _filesWhitelist
<< ", internal read whitelist:" << _readWhitelist
<< ", internal write whitelist:" << _writeWhitelist
<< ", internal startup options whitelist:" << _startupOptionsWhitelist
<< ", internal startup options blacklist: " << _startupOptionsBlacklist
<< ", internal environment variable whitelist:" << _environmentVariablesWhitelist
<< ", internal environment variables blacklist: " << _environmentVariablesBlacklist
<< ", internal endpoints whitelist:" << _endpointsWhitelist
<< ", internal endpoints blacklist: " << _endpointsBlacklist;
}
void V8SecurityFeature::addToInternalWhitelist(std::string const& inItem, FSAccessType type) {
// This function is not efficient and we would not need the _readWhitelist
// to be persistent. But the persistence will help in debugging and
// there are only a few items expected.
auto* set = &_readWhitelistSet;
auto* expression = &_readWhitelist;
auto* re = &_readWhitelistRegex;
if (type == FSAccessType::WRITE) {
set = &_writeWhitelistSet;
expression = &_writeWhitelist;
re = &_writeWhitelistRegex;
}
auto item = canonicalpath(inItem);
if ((item.length() > 0) &&
(item[item.length() - 1] != TRI_DIR_SEPARATOR_CHAR)) {
item += TRI_DIR_SEPARATOR_STR;
}
auto path = "^" + arangodb::basics::StringUtils::escapeRegexParams(item);
set->emplace(std::move(path));
expression->clear();
convertToSingleExpression(*set, *expression);
try {
*re = std::regex(*expression, std::regex::nosubs | std::regex::ECMAScript);
} catch (std::exception const& ex) {
throw std::invalid_argument(ex.what() + std::string(" '") + *expression + "'");
}
}
bool V8SecurityFeature::isAllowedToControlProcesses(v8::Isolate* isolate) const {
TRI_GET_GLOBALS();
TRI_ASSERT(v8g != nullptr);
return _allowProcessControl && v8g->_securityContext.canControlProcesses();
}
bool V8SecurityFeature::isAllowedToTestPorts(v8::Isolate* isolate) const {
return _allowPortTesting;
}
bool V8SecurityFeature::isInternalModuleHardened(v8::Isolate* isolate) const {
return _hardenInternalModule;
}
bool V8SecurityFeature::isAllowedToDefineHttpAction(v8::Isolate* isolate) const {
TRI_GET_GLOBALS();
TRI_ASSERT(v8g != nullptr);
return v8g->_securityContext.canDefineHttpAction();
}
bool V8SecurityFeature::isInternalContext(v8::Isolate* isolate) const {
TRI_GET_GLOBALS();
TRI_ASSERT(v8g != nullptr);
return v8g->_securityContext.isInternal();
}
bool V8SecurityFeature::shouldExposeStartupOption(v8::Isolate* isolate,
std::string const& name) const {
return checkBlackAndWhitelist(name, !_startupOptionsWhitelist.empty(),
_startupOptionsWhitelistRegex,
!_startupOptionsBlacklist.empty(),
_startupOptionsBlacklistRegex);
}
bool V8SecurityFeature::shouldExposeEnvironmentVariable(v8::Isolate* isolate,
std::string const& name) const {
return checkBlackAndWhitelist(name, !_environmentVariablesWhitelist.empty(),
_environmentVariablesWhitelistRegex,
!_environmentVariablesBlacklist.empty(),
_environmentVariablesBlacklistRegex);
}
bool V8SecurityFeature::isAllowedToConnectToEndpoint(v8::Isolate* isolate,
std::string const& name) const {
TRI_GET_GLOBALS();
TRI_ASSERT(v8g != nullptr);
if (v8g->_securityContext.isInternal()) {
// internal security contexts are allowed to connect to any endpoint
// this includes connecting to self or to other instances in a cluster
return true;
}
return checkBlackAndWhitelist(name, !_endpointsWhitelist.empty(), _endpointsWhitelistRegex,
!_endpointsBlacklist.empty(), _endpointsBlacklistRegex);
}
bool V8SecurityFeature::isAllowedToAccessPath(v8::Isolate* isolate, std::string const& path,
FSAccessType access) const {
return V8SecurityFeature::isAllowedToAccessPath(isolate, path.c_str(), access);
}
bool V8SecurityFeature::isAllowedToAccessPath(v8::Isolate* isolate, char const* pathPtr,
FSAccessType access) const {
if (_filesWhitelist.empty()) {
return true;
}
// check security context first
TRI_GET_GLOBALS();
TRI_ASSERT(v8g != nullptr);
auto const& sec = v8g->_securityContext;
if ((access == FSAccessType::READ && sec.canReadFs()) ||
(access == FSAccessType::WRITE && sec.canWriteFs())) {
return true; // context may read / write without restrictions
}
std::string path = TRI_ResolveSymbolicLink(pathPtr);
// make absolute
std::string cwd = FileUtils::currentDirectory().result();
path = TRI_GetAbsolutePath(path, cwd);
path = canonicalpath(std::move(path));
if (basics::FileUtils::isDirectory(path)) {
path += TRI_DIR_SEPARATOR_STR;
}
if (access == FSAccessType::READ && std::regex_search(path, _readWhitelistRegex)) {
// even in restricted contexts we may read module paths
return true;
}
if (access == FSAccessType::WRITE && std::regex_search(path, _writeWhitelistRegex)) {
// even in restricted contexts we may read module paths
return true;
}
return checkBlackAndWhitelist(path, !_filesWhitelist.empty(), _filesWhitelistRegex,
false, _filesWhitelistRegex /*passed to match the signature but not used*/);
}