From 09443c31bdb7c89ff91103a1b98ade9aca27a8bf Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 1 Oct 2020 17:57:51 -0700 Subject: [PATCH 1/3] [WIP] src: introduce Policy permissions --- lib/internal/bootstrap/loaders.js | 11 +- lib/internal/bootstrap/node.js | 5 +- lib/internal/bootstrap/pre_execution.js | 12 +- lib/internal/process/policy.js | 29 ++++ node.gyp | 3 + src/env-inl.h | 5 + src/env.cc | 1 + src/env.h | 7 + src/node.cc | 35 +++- src/node_binding.cc | 1 + src/node_errors.h | 2 + src/node_external_reference.h | 1 + src/node_native_module.cc | 3 +- src/node_options.cc | 8 + src/node_options.h | 3 + src/policy/policy-inl.h | 218 ++++++++++++++++++++++++ src/policy/policy.cc | 165 ++++++++++++++++++ src/policy/policy.h | 197 +++++++++++++++++++++ 18 files changed, 693 insertions(+), 13 deletions(-) create mode 100644 src/policy/policy-inl.h create mode 100644 src/policy/policy.cc create mode 100644 src/policy/policy.h diff --git a/lib/internal/bootstrap/loaders.js b/lib/internal/bootstrap/loaders.js index cae3200e677926..b995d6acd58df8 100644 --- a/lib/internal/bootstrap/loaders.js +++ b/lib/internal/bootstrap/loaders.js @@ -41,7 +41,8 @@ // This file is compiled as if it's wrapped in a function with arguments // passed by node::RunBootstrapping() -/* global process, getLinkedBinding, getInternalBinding, primordials */ +/* global process, getLinkedBinding, getInternalBinding, primordials, + runInPrivilegedScope */ const { ArrayPrototypeMap, @@ -280,7 +281,13 @@ class NativeModule { requireWithFallbackInDeps : nativeModuleRequire; const fn = compileFunction(id); - fn(this.exports, requireFn, this, process, internalBinding, primordials); + fn(this.exports, + requireFn, + this, + process, + internalBinding, + primordials, + runInPrivilegedScope); this.loaded = true; } finally { diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index 8077c462983154..23efd1c9fabc47 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -33,8 +33,9 @@ 'use strict'; // This file is compiled as if it's wrapped in a function with arguments -// passed by node::RunBootstrapping() -/* global process, require, internalBinding, primordials */ +// passed by node::RunBootstrapping(): +// global process, require, internalBinding, primordials, +// runInPriviledgedScope setupPrepareStackTrace(); diff --git a/lib/internal/bootstrap/pre_execution.js b/lib/internal/bootstrap/pre_execution.js index 381a54d489fb0f..b9bd4ebe0ce645 100644 --- a/lib/internal/bootstrap/pre_execution.js +++ b/lib/internal/bootstrap/pre_execution.js @@ -356,6 +356,7 @@ function initializeClusterIPC() { function initializePolicy() { const experimentalPolicy = getOptionValue('--experimental-policy'); + const { setup, check, deny } = require('internal/process/policy'); if (experimentalPolicy) { process.emitWarning('Policies are experimental.', 'ExperimentalWarning'); @@ -398,9 +399,16 @@ function initializePolicy() { throw new ERR_MANIFEST_ASSERT_INTEGRITY(manifestURL, realIntegrities); } } - require('internal/process/policy') - .setup(src, manifestURL.href); + setup(src, manifestURL.href); } + ObjectDefineProperty(process, 'policy', { + enumerable: true, + configurable: false, + value: { + deny, + check, + } + }); } function initializeWASI() { diff --git a/lib/internal/process/policy.js b/lib/internal/process/policy.js index ea283a449742fc..d59a400143ed1f 100644 --- a/lib/internal/process/policy.js +++ b/lib/internal/process/policy.js @@ -8,7 +8,12 @@ const { const { ERR_MANIFEST_TDZ, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, } = require('internal/errors').codes; + +const policy = internalBinding('policy'); + const { Manifest } = require('internal/policy/manifest'); let manifest; let manifestSrc; @@ -57,5 +62,29 @@ module.exports = ObjectFreeze({ assertIntegrity(moduleURL, content) { this.manifest.assertIntegrity(moduleURL, content); + }, + + deny(permissions) { + if (typeof permissions !== 'string') + throw new ERR_INVALID_ARG_TYPE('permissions', 'string', permissions); + const ret = policy.deny(permissions); + if (typeof ret === 'number') + throw new ERR_INVALID_ARG_VALUE('permissions', permissions); + }, + + fastCheck(permission) { + // This should only be used by internal code. Skips explicit + // type checking to improve performance. The permission + // argument must be a Int32 + return policy.fastCheck(permission); + }, + + check(permissions) { + if (typeof permissions !== 'string') + throw new ERR_INVALID_ARG_TYPE('permission', 'string', permissions); + const ret = policy.check(permissions); + if (typeof ret === 'number') + throw new ERR_INVALID_ARG_VALUE('permissions', permissions); + return ret; } }); diff --git a/node.gyp b/node.gyp index 865a7de93176e5..2090f10e5333f9 100644 --- a/node.gyp +++ b/node.gyp @@ -660,6 +660,7 @@ 'src/stream_wrap.cc', 'src/string_bytes.cc', 'src/string_decoder.cc', + 'src/policy/policy.cc', 'src/tcp_wrap.cc', 'src/timers.cc', 'src/timer_wrap.cc', @@ -735,6 +736,8 @@ 'src/node_perf.h', 'src/node_perf_common.h', 'src/node_platform.h', + 'src/policy/policy.h', + 'src/policy/policy-inl.h', 'src/node_process.h', 'src/node_report.h', 'src/node_revert.h', diff --git a/src/env-inl.h b/src/env-inl.h index cd9f6daaaff758..4568b0a8aabf32 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -29,6 +29,7 @@ #include "env.h" #include "node.h" #include "util-inl.h" +#include "policy/policy-inl.h" #include "uv.h" #include "v8.h" #include "node_perf_common.h" @@ -246,6 +247,10 @@ inline size_t Environment::async_callback_scope_depth() const { return async_callback_scope_depth_; } +policy::PrivilegedAccessContext* Environment::privileged_access_context() { + return &privileged_access_context_; +} + inline void Environment::PushAsyncCallbackScope() { async_callback_scope_depth_++; } diff --git a/src/env.cc b/src/env.cc index 7e89c2828f2003..48dd2aaac506bd 100644 --- a/src/env.cc +++ b/src/env.cc @@ -313,6 +313,7 @@ Environment::Environment(IsolateData* isolate_data, ThreadId thread_id) : isolate_(isolate), isolate_data_(isolate_data), + privileged_access_context_(isolate_data->options()->per_env.get()), async_hooks_(isolate, MAYBE_FIELD_PTR(env_info, async_hooks)), immediate_info_(isolate, MAYBE_FIELD_PTR(env_info, immediate_info)), tick_info_(isolate, MAYBE_FIELD_PTR(env_info, tick_info)), diff --git a/src/env.h b/src/env.h index a0d59ff8728deb..7208ff6de920ec 100644 --- a/src/env.h +++ b/src/env.h @@ -37,6 +37,7 @@ #include "node_main_instance.h" #include "node_options.h" #include "node_perf_common.h" +#include "policy/policy.h" #include "req_wrap.h" #include "util.h" #include "uv.h" @@ -375,6 +376,7 @@ constexpr size_t kFsStatsBufferLength = V(replacement_string, "replacement") \ V(require_string, "require") \ V(retry_string, "retry") \ + V(run_in_privileged_scope_string, "runInPrivilegedScope") \ V(scheme_string, "scheme") \ V(scopeid_string, "scopeid") \ V(serial_number_string, "serialNumber") \ @@ -547,6 +549,7 @@ constexpr size_t kFsStatsBufferLength = V(primordials, v8::Object) \ V(promise_hook_handler, v8::Function) \ V(promise_reject_callback, v8::Function) \ + V(run_in_privileged_scope, v8::Function) \ V(script_data_constructor_function, v8::Function) \ V(source_map_cache_getter, v8::Function) \ V(tick_callback_function, v8::Function) \ @@ -977,6 +980,7 @@ class Environment : public MemoryRetainer { v8::MaybeLocal BootstrapInternalLoaders(); v8::MaybeLocal BootstrapNode(); v8::MaybeLocal RunBootstrapping(); + bool BootstrapPrivilegedAccessContext(); inline size_t async_callback_scope_depth() const; inline void PushAsyncCallbackScope(); @@ -1036,6 +1040,8 @@ class Environment : public MemoryRetainer { inline const std::vector& argv(); const std::string& exec_path() const; + inline policy::PrivilegedAccessContext* privileged_access_context(); + typedef void (*HandleCleanupCb)(Environment* env, uv_handle_t* handle, void* arg); @@ -1397,6 +1403,7 @@ class Environment : public MemoryRetainer { std::list loaded_addons_; v8::Isolate* const isolate_; IsolateData* const isolate_data_; + policy::PrivilegedAccessContext privileged_access_context_; uv_timer_t timer_handle_; uv_check_t immediate_check_handle_; uv_idle_t immediate_idle_handle_; diff --git a/src/node.cc b/src/node.cc index c3f423cb579479..af7b5616230554 100644 --- a/src/node.cc +++ b/src/node.cc @@ -39,6 +39,7 @@ #include "node_revert.h" #include "node_v8_platform-inl.h" #include "node_version.h" +#include "policy/policy.h" #if HAVE_OPENSSL #include "allocated_buffer-inl.h" // Inlined functions needed by node_crypto.h @@ -294,6 +295,18 @@ void Environment::InitializeDiagnostics() { #endif } +bool Environment::BootstrapPrivilegedAccessContext() { + Local run_in_privileged_scope; + MaybeLocal maybe_run_in_privileged_scope = + Function::New( + context(), + policy::PrivilegedAccessContext::Run); + if (!maybe_run_in_privileged_scope.ToLocal(&run_in_privileged_scope)) + return false; + set_run_in_privileged_scope(run_in_privileged_scope); + return true; +} + MaybeLocal Environment::BootstrapInternalLoaders() { EscapableHandleScope scope(isolate_); @@ -302,7 +315,8 @@ MaybeLocal Environment::BootstrapInternalLoaders() { process_string(), FIXED_ONE_BYTE_STRING(isolate_, "getLinkedBinding"), FIXED_ONE_BYTE_STRING(isolate_, "getInternalBinding"), - primordials_string()}; + primordials_string(), + run_in_privileged_scope_string()}; std::vector> loaders_args = { process_object(), NewFunctionTemplate(binding::GetLinkedBinding) @@ -311,7 +325,8 @@ MaybeLocal Environment::BootstrapInternalLoaders() { NewFunctionTemplate(binding::GetInternalBinding) ->GetFunction(context()) .ToLocalChecked(), - primordials()}; + primordials(), + run_in_privileged_scope()}; // Bootstrap internal loaders Local loader_exports; @@ -348,12 +363,14 @@ MaybeLocal Environment::BootstrapNode() { process_string(), require_string(), internal_binding_string(), - primordials_string()}; + primordials_string(), + run_in_privileged_scope_string()}; std::vector> node_args = { process_object(), native_module_require(), internal_binding_loader(), - primordials()}; + primordials(), + run_in_privileged_scope()}; MaybeLocal result = ExecuteBootstrapper( this, "internal/bootstrap/node", &node_params, &node_args); @@ -399,6 +416,10 @@ MaybeLocal Environment::RunBootstrapping() { CHECK(!has_run_bootstrapping_code()); + if (!BootstrapPrivilegedAccessContext()) { + return MaybeLocal(); + } + if (BootstrapInternalLoaders().IsEmpty()) { return MaybeLocal(); } @@ -436,7 +457,8 @@ MaybeLocal StartExecution(Environment* env, const char* main_script_id) { env->require_string(), env->internal_binding_string(), env->primordials_string(), - FIXED_ONE_BYTE_STRING(env->isolate(), "markBootstrapComplete")}; + FIXED_ONE_BYTE_STRING(env->isolate(), "markBootstrapComplete"), + env->run_in_privileged_scope_string()}; std::vector> arguments = { env->process_object(), @@ -445,7 +467,8 @@ MaybeLocal StartExecution(Environment* env, const char* main_script_id) { env->primordials(), env->NewFunctionTemplate(MarkBootstrapComplete) ->GetFunction(env->context()) - .ToLocalChecked()}; + .ToLocalChecked(), + env->run_in_privileged_scope()}; return scope.EscapeMaybe( ExecuteBootstrapper(env, main_script_id, ¶meters, &arguments)); diff --git a/src/node_binding.cc b/src/node_binding.cc index 0db930adff1a9f..3bf7fb76393335 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -69,6 +69,7 @@ V(os) \ V(performance) \ V(pipe_wrap) \ + V(policy) \ V(process_wrap) \ V(process_methods) \ V(report) \ diff --git a/src/node_errors.h b/src/node_errors.h index 6158a968d27a9a..208fb2ca579609 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -28,6 +28,7 @@ void OnFatalError(const char* location, const char* message); // a `Local` containing the TypeError with proper code and message #define ERRORS_WITH_CODE(V) \ + V(ERR_ACCESS_DENIED, Error) \ V(ERR_BUFFER_CONTEXT_NOT_AVAILABLE, Error) \ V(ERR_BUFFER_OUT_OF_BOUNDS, RangeError) \ V(ERR_BUFFER_TOO_LARGE, Error) \ @@ -105,6 +106,7 @@ void OnFatalError(const char* location, const char* message); // Errors with predefined static messages #define PREDEFINED_ERROR_MESSAGES(V) \ + V(ERR_ACCESS_DENIED, "Access is denied") \ V(ERR_BUFFER_CONTEXT_NOT_AVAILABLE, \ "Buffer is not available for the current Context") \ V(ERR_CONSTRUCT_CALL_INVALID, "Constructor cannot be called") \ diff --git a/src/node_external_reference.h b/src/node_external_reference.h index 0544979dd9a6f1..5477bc89f5e694 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -56,6 +56,7 @@ class ExternalReferenceRegistry { V(handle_wrap) \ V(messaging) \ V(native_module) \ + V(policy) \ V(process_methods) \ V(process_object) \ V(task_queue) \ diff --git a/src/node_native_module.cc b/src/node_native_module.cc index f7d73544d2dbf1..8a9ecfc87871d1 100644 --- a/src/node_native_module.cc +++ b/src/node_native_module.cc @@ -183,7 +183,8 @@ MaybeLocal NativeModuleLoader::CompileAsModule( FIXED_ONE_BYTE_STRING(isolate, "module"), FIXED_ONE_BYTE_STRING(isolate, "process"), FIXED_ONE_BYTE_STRING(isolate, "internalBinding"), - FIXED_ONE_BYTE_STRING(isolate, "primordials")}; + FIXED_ONE_BYTE_STRING(isolate, "primordials"), + FIXED_ONE_BYTE_STRING(isolate, "runInPrivilegedScope")}; return LookupAndCompile(context, id, ¶meters, result); } diff --git a/src/node_options.cc b/src/node_options.cc index e90dcd93231fca..746c324af36af4 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -322,6 +322,14 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::experimental_policy_integrity, kAllowedInEnvironment); Implies("--policy-integrity", "[has_policy_integrity_string]"); + AddOption("--policy-deny", + "denied permissions", + &EnvironmentOptions::policy_deny, + kAllowedInEnvironment); + AddOption("--policy-grant", + "granted permissions", + &EnvironmentOptions::policy_grant, + kAllowedInEnvironment); AddOption("--experimental-repl-await", "experimental await keyword support in REPL", &EnvironmentOptions::experimental_repl_await, diff --git a/src/node_options.h b/src/node_options.h index 84ee8e34bcafcf..112899350e1177 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -177,6 +177,9 @@ class EnvironmentOptions : public Options { std::vector preload_modules; + std::string policy_deny; + std::string policy_grant; + std::vector user_argv; inline DebugOptions* get_debug_options() { return &debug_options_; } diff --git a/src/policy/policy-inl.h b/src/policy/policy-inl.h new file mode 100644 index 00000000000000..a00c4d741a30bc --- /dev/null +++ b/src/policy/policy-inl.h @@ -0,0 +1,218 @@ +#ifndef SRC_POLICY_POLICY_INL_H_ +#define SRC_POLICY_POLICY_INL_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "policy/policy.h" +#include "env-inl.h" +#include "util-inl.h" +#include "node_errors.h" + +#include + +namespace node { +namespace policy { + +Policy::Policy(EnvironmentOptions* options) { + Apply(options->policy_deny, options->policy_grant); +} + +Policy::Policy( + Policy* basis, + const std::string& deny, + const std::string& grant) { + permissions_ = basis->permissions_; + Apply(deny, grant, ApplyFlags::kIgnoreSpecials); +} + +Policy::Policy( + Policy* basis, + const std::vector& deny, + const std::vector& grant) { + permissions_ = basis->permissions_; + Apply(deny, grant, ApplyFlags::kIgnoreSpecials); +} + +bool Policy::test(Permission permission) const { + return UNLIKELY(permissions_.test(static_cast(permission))); +} + +#define V(name, _, parent) \ + if (permission == Permission::k##parent) \ + SetRecursively(Permission::k##name, value); +void Policy::SetRecursively(Permission permission, bool value) { + if (permission != Permission::kPermissionsRoot) + permissions_[static_cast(permission)] = value; + PERMISSIONS(V) +} +#undef V + +void Policy::Grant(Permission permission) { + if (LIKELY(locked_)) + return; + if (UNLIKELY(permission == Permission::kPermissionsCount)) + return; + SetRecursively(permission, false); +} + +void Policy::Grant(std::string permission) { + Grant(PermissionFromName(permission)); +} + +void Policy::Deny(Permission permission) { + if (UNLIKELY(permission == Permission::kPermissionsCount)) + return; + SetRecursively(permission, true); +} + +void Policy::Deny(std::string permission) { + Deny(PermissionFromName(permission)); +} + +bool Policy::is_granted(Permission permission) const { + return LIKELY(permission != Permission::kPermissionsCount) && + LIKELY(permission != Permission::kPermissionsRoot) && + !test(permission); +} + +bool Policy::is_granted(std::string permission) const { + return is_granted(PermissionFromName(permission)); +} + +void Policy::Apply( + const std::string& deny, + const std::string& grant, + ApplyFlags flags) { + std::vector denied = Policy::Parse(deny); + std::vector granted = Policy::Parse(grant); + Apply(denied, granted); +} + +void Policy::Apply( + const std::vector& deny, + const std::vector& grant, + ApplyFlags flags) { + + // The entire special category is denied by default if there + // are any other denials (and the flag is set); + if (deny.size() > 0 && flags == ApplyFlags::kDenySpecials) + Deny(Permission::kSpecial); + + for (Permission permission : deny) + Deny(permission); + + for (Permission permission : grant) + Grant(permission); + Lock(); +} + +#define V(Name, label, _) \ + if (strcmp(name.c_str(), label) == 0) return Permission::k##Name; +Permission Policy::PermissionFromName(const std::string& name) { + if (strcmp(name.c_str(), "*") == 0) return Permission::kPermissionsRoot; + PERMISSIONS(V) + return Permission::kPermissionsCount; +} +#undef V + +std::vector Policy::Parse( + const std::string& list, + ParseStatus* status) { + std::vector permissions; + for (auto name : SplitString(list, ',')) { + Permission permission = PermissionFromName(name); + if (permission != Permission::kPermissionsCount) + permissions.push_back(permission); + else if (status != nullptr) + *status = ParseStatus::UNKNOWN; + } + return permissions; +} + +PrivilegedAccessContext::PrivilegedAccessContext( + EnvironmentOptions* options) { + std::unique_ptr policy = std::make_unique(options); + Push(std::move(policy)); +} + +// Push a new Policy onto the stack. The new policy starts as a +// clone of the current Policy with the additionally given +// denials/grants applied. +void PrivilegedAccessContext::Push( + const std::string& deny, + const std::string& grant) { + std::unique_ptr policy = + std::make_unique( + policy_stack_.front().get(), + deny, + grant); + Push(std::move(policy)); +} + +// Push a new Policy onto the stack. The new policy starts as a +// clone of the current Policy with the additionally given +// denials/grants applied. +void PrivilegedAccessContext::Push( + const std::vector& deny, + const std::vector& grant) { + std::unique_ptr policy = + std::make_unique( + policy_stack_.front().get(), + deny, + grant); + Push(std::move(policy)); +} + +void PrivilegedAccessContext::Push(std::unique_ptr policy) { + policy_stack_.push_front(std::move(policy)); +} + +bool PrivilegedAccessContext::Pop() { + // Can't remove the root Policy + if (policy_stack_.size() == 1) + return false; + policy_stack_.pop_front(); + return true; +} + +bool PrivilegedAccessContext::is_granted(Permission permission) { + return policy_stack_.front()->is_granted(permission); +} + +bool PrivilegedAccessContext::is_granted(const std::string& permission) { + return policy_stack_.front()->is_granted(permission); +} + +void PrivilegedAccessContext::Deny(Permission permission) { + policy_stack_.front()->Deny(permission); +} + +void PrivilegedAccessContext::Deny(const std::string& permission) { + policy_stack_.front()->Deny(permission); +} + +PrivilegedAccessContext::Scope::Scope( + Environment* env, + const std::string& deny, + const std::string& grant) + : env_(env) { + env_->privileged_access_context()->Push(deny, grant); +} + +PrivilegedAccessContext::Scope::Scope( + Environment* env, + const std::vector& deny, + const std::vector& grant) + : env_(env) { + env_->privileged_access_context()->Push(deny, grant); +} + +PrivilegedAccessContext::Scope::~Scope() { + env_->privileged_access_context()->Pop(); +} + +} // namespace policy +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_POLICY_POLICY_INL_H_ diff --git a/src/policy/policy.cc b/src/policy/policy.cc new file mode 100644 index 00000000000000..88d0de00f39a07 --- /dev/null +++ b/src/policy/policy.cc @@ -0,0 +1,165 @@ +#include "policy-inl.h" // NOLINT(build/include) +#include "aliased_struct-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node.h" +#include "node_external_reference.h" + +#include "v8.h" + +#include +#include + +namespace node { + +using v8::Array; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::Int32; +using v8::Local; +using v8::MaybeLocal; +using v8::Object; +using v8::Value; + +namespace policy { + +namespace { +Permission GetPermission(Local arg) { + if (!arg->IsInt32()) + return Permission::kPermissionsCount; + int32_t permission = arg.As()->Value(); + if (permission < static_cast(Permission::kPermissionsRoot) || + permission > static_cast(Permission::kPermissionsCount)) { + return Permission::kPermissionsCount; + } + return static_cast(permission); +} +} // namespace + +static void Deny(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString()); + Utf8Value list(env->isolate(), args[0]); + Policy::ParseStatus status = Policy::ParseStatus::OK; + auto permissions = Policy::Parse(*list, &status); + if (status != Policy::ParseStatus::OK) + return args.GetReturnValue().Set(static_cast(status)); + for (Permission permission : permissions) + env->privileged_access_context()->Deny(permission); +} + +static void FastCheck(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Permission permission = GetPermission(args[0]); + CHECK_LT(permission, Permission::kPermissionsCount); + CHECK_GT(permission, Permission::kPermissionsRoot); + args.GetReturnValue().Set( + env->privileged_access_context()->is_granted(GetPermission(args[0]))); +} + +static void Check(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsString()); + Utf8Value list(env->isolate(), args[0]); + Policy::ParseStatus status = Policy::ParseStatus::OK; + auto permissions = Policy::Parse(*list, &status); + if (status != Policy::ParseStatus::OK) + return args.GetReturnValue().Set(static_cast(status)); + for (Permission permission : permissions) { + if (!env->privileged_access_context()->is_granted(permission)) + return args.GetReturnValue().Set(false); + } + args.GetReturnValue().Set(true); +} + +namespace { +std::vector ToPermissions(Environment* env, Local input) { + std::vector permissions; + for (size_t n = 0; n < input->Length(); n++) { + Local val = input->Get(env->context(), n).FromMaybe(Local()); + CHECK(val->IsInt32()); + int32_t permission = val.As()->Value(); + CHECK_GE(permission, static_cast(Permission::kPermissionsRoot)); + CHECK_LT(permission, static_cast(Permission::kPermissionsCount)); + permissions.push_back(static_cast(permission)); + } + return permissions; +} +} // namespace + +// The starting arguments must consist of a function along with +// two strings identifying the permissions to deny or grant. +// The remaining arguments will be passed on to the function, +void PrivilegedAccessContext::Run( + const v8::FunctionCallbackInfo& args) { + CHECK_GE(args.Length(), 3); + CHECK(args[0]->IsString() || args[0]->IsArray()); // The denials + CHECK_IMPLIES(args[0]->IsString(), args[1]->IsString()); + CHECK_IMPLIES(args[1]->IsArray(), args[1]->IsArray()); + CHECK(args[2]->IsFunction()); // The function to execute + + Local fn = args[2].As(); + + CHECK(!args.IsConstructCall()); + + SlicedArguments call_args(args, 3); + Environment* env = Environment::GetCurrent(args); + MaybeLocal ret; + + if (args[0]->IsArray()) { + std::vector deny = ToPermissions(env, args[0].As()); + std::vector grant = ToPermissions(env, args[1].As()); + PrivilegedAccessContext::Scope privileged_scope(env, deny, grant); + ret = fn->Call( + env->context(), + args.This(), + call_args.length(), + call_args.out()); + } else { + Utf8Value deny(env->isolate(), args[0]); + Utf8Value grant(env->isolate(), args[1]); + PrivilegedAccessContext::Scope privileged_scope(env, *deny, *grant); + ret = fn->Call( + env->context(), + args.This(), + call_args.length(), + call_args.out()); + } + + args.GetReturnValue().Set(ret.FromMaybe(Local())); +} + +void Initialize(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + env->SetMethod(target, "deny", Deny); + env->SetMethodNoSideEffect(target, "fastCheck", FastCheck); + env->SetMethodNoSideEffect(target, "check", Check); + + #define V(name, _, __) \ + constexpr int kPermission##name = static_cast(Permission::k##name); \ + NODE_DEFINE_CONSTANT(target, kPermission##name); + PERMISSIONS(V) + #undef V + + // internalBinding('policy') should be frozen + target->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen).FromJust(); +} + +void RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(Deny); + registry->Register(FastCheck); + registry->Register(Check); + registry->Register(PrivilegedAccessContext::Run); +} + +} // namespace policy +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(policy, node::policy::Initialize) +NODE_MODULE_EXTERNAL_REFERENCE(policy, node::policy::RegisterExternalReferences) diff --git a/src/policy/policy.h b/src/policy/policy.h new file mode 100644 index 00000000000000..2cce7606eedbed --- /dev/null +++ b/src/policy/policy.h @@ -0,0 +1,197 @@ +#ifndef SRC_POLICY_POLICY_H_ +#define SRC_POLICY_POLICY_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node_options.h" +#include "v8.h" + +#include +#include +#include +#include + +namespace node { +namespace policy { + +// Special Permissions are denied by default if any other permission is denied. +#define SPECIAL_PERMISSIONS(V) \ + V(Special, "special", PermissionsRoot) \ + V(SpecialInspector, "special.inspector", Special) \ + V(SpecialAddons, "special.addons", Special) \ + V(SpecialChildProcess, "special.child_process", Special) + +#define FILESYSTEM_PERMISSIONS(V) \ + V(FileSystem, "fs", PermissionsRoot) \ + V(FileSystemIn, "fs.in", FileSystem) \ + V(FileSystemOut, "fs.out", FileSystem) + +#define NETWORKING_PERMISSIONS(V) \ + V(Net, "net", PermissionsRoot) \ + V(NetUdp, "net.udp", Net) \ + V(NetDNS, "net.dns", Net) \ + V(NetTCP, "net.tcp", Net) \ + V(NetTCPIn, "net.tcp.in", NetTCP) \ + V(NetTCPOut, "net.tcp.out", NetTCP) \ + V(NetTLS, "net.tls", Net) \ + V(NetTLSLog, "net.tls.log", NetTLS) + +#define EXPERIMENTAL_PERMISSIONS(V) \ + V(Experimental, "experimental", PermissionsRoot) \ + V(ExperimentalWasi, "experimental.wasi", Experimental) + +#define PERMISSIONS(V) \ + EXPERIMENTAL_PERMISSIONS(V) \ + FILESYSTEM_PERMISSIONS(V) \ + NETWORKING_PERMISSIONS(V) \ + SPECIAL_PERMISSIONS(V) \ + V(Process, "process", PermissionsRoot) \ + V(Signal, "signal", PermissionsRoot) \ + V(Timing, "timing", PermissionsRoot) \ + V(User, "user", PermissionsRoot) \ + V(Workers, "workers", PermissionsRoot) \ + +#define V(name, _, __) k##name, +enum class Permission { + kPermissionsRoot = -1, + PERMISSIONS(V) + kPermissionsCount +}; +#undef V + +class Policy final { + public: + enum class ParseStatus { + OK, + UNKNOWN + }; + + static inline Permission PermissionFromName(const std::string& name); + + static inline std::vector Parse( + const std::string& list, + ParseStatus* status = nullptr); + + inline explicit Policy(EnvironmentOptions* options); + + inline Policy( + Policy* basis, + const std::string& deny, + const std::string& grant); + + inline Policy( + Policy* basis, + const std::vector& deny, + const std::vector& grant); + + Policy(Policy&& other) = delete; + Policy& operator=(Policy&& other) = delete; + Policy(const Policy& other) = delete; + Policy& operator=(const Policy& other) = delete; + + inline void Deny(Permission permission); + inline void Deny(std::string permission); + + inline void Grant(Permission permission); + inline void Grant(std::string permission); + + // Once a policy is locked, no additional grants will be permitted. + inline void Lock() { locked_ = true; } + + inline bool is_granted(Permission permission) const; + inline bool is_granted(std::string permission) const; + + // Once a policy is locked, no additional grants will be permitted. + inline bool is_locked() const { return locked_; } + + private: + inline void MaybeDenySpecials(Policy* policy, Permission permission); + + enum class ApplyFlags { + // When used, instructs Apply not to deny specials automatically + kIgnoreSpecials, + // When used, instructs Apply to deny specials if there are any + // other denials + kDenySpecials + }; + + inline void Apply( + const std::string& deny, + const std::string& grant, + ApplyFlags flags = ApplyFlags::kDenySpecials); + + inline void Apply( + const std::vector& deny, + const std::vector& grant, + ApplyFlags flags = ApplyFlags::kDenySpecials); + + inline bool test(Permission permission) const; + inline void SetRecursively(Permission permission, bool value); + + bool locked_ = false; + std::bitset(Permission::kPermissionsCount)> permissions_; +}; + +// The Environment always has exactly one PriviledgeAccessContext that covers +// the currently active Policy. Think of the PrivilegedAccessContext as a +// stack of Policy objects with the default or root policy as the base. +class PrivilegedAccessContext { + public: + class Scope { + public: + inline Scope( + Environment* env, + const std::string& deny, + const std::string& grant); + + inline Scope( + Environment* env, + const std::vector& deny, + const std::vector& grant); + + inline ~Scope(); + private: + Environment* env_; + }; + + static void Run(const v8::FunctionCallbackInfo& args); + + inline explicit PrivilegedAccessContext(EnvironmentOptions* options); + + PrivilegedAccessContext( + PrivilegedAccessContext&& other) = delete; + PrivilegedAccessContext& operator=( + PrivilegedAccessContext&& other) = delete; + PrivilegedAccessContext( + const PrivilegedAccessContext& other) = delete; + PrivilegedAccessContext& operator=( + const PrivilegedAccessContext& other) = delete; + + inline void Push( + const std::string& deny, + const std::string& grant); + + inline void Push( + const std::vector& deny, + const std::vector& grant); + + inline bool Pop(); + + inline bool is_granted(Permission permission); + + inline bool is_granted(const std::string& permission); + + inline void Deny(Permission permission); + + inline void Deny(const std::string& permission); + + private: + inline void Push(std::unique_ptr policy); + std::deque> policy_stack_; +}; + +} // namespace policy +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_POLICY_POLICY_H_ From 290637f5003c7bdbc6e8a7b16896a6d52720d740 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 11 Dec 2020 14:39:39 -0800 Subject: [PATCH 2/3] [WIP] Refactor and simplify the implementation --- lib/.eslintrc.yaml | 1 + lib/internal/process/policy.js | 4 +- lib/internal/test/policy.js | 7 ++ node.gyp | 2 +- src/env-inl.h | 9 +- src/env.cc | 1 - src/env.h | 9 +- src/node.cc | 12 +- src/node_options.cc | 16 +-- src/node_options.h | 6 +- src/policy/policy-inl.h | 218 --------------------------------- src/policy/policy.cc | 193 +++++++++++++++++++---------- src/policy/policy.h | 179 +++++++++------------------ 13 files changed, 226 insertions(+), 431 deletions(-) create mode 100644 lib/internal/test/policy.js delete mode 100644 src/policy/policy-inl.h diff --git a/lib/.eslintrc.yaml b/lib/.eslintrc.yaml index deea6bf4fb05ae..2b62bfb0eca38f 100644 --- a/lib/.eslintrc.yaml +++ b/lib/.eslintrc.yaml @@ -93,3 +93,4 @@ globals: module: false internalBinding: false primordials: false + runInPrivilegedScope: false diff --git a/lib/internal/process/policy.js b/lib/internal/process/policy.js index d59a400143ed1f..14acd7a69479f6 100644 --- a/lib/internal/process/policy.js +++ b/lib/internal/process/policy.js @@ -68,7 +68,7 @@ module.exports = ObjectFreeze({ if (typeof permissions !== 'string') throw new ERR_INVALID_ARG_TYPE('permissions', 'string', permissions); const ret = policy.deny(permissions); - if (typeof ret === 'number') + if (ret === undefined) throw new ERR_INVALID_ARG_VALUE('permissions', permissions); }, @@ -83,7 +83,7 @@ module.exports = ObjectFreeze({ if (typeof permissions !== 'string') throw new ERR_INVALID_ARG_TYPE('permission', 'string', permissions); const ret = policy.check(permissions); - if (typeof ret === 'number') + if (ret === undefined) throw new ERR_INVALID_ARG_VALUE('permissions', permissions); return ret; } diff --git a/lib/internal/test/policy.js b/lib/internal/test/policy.js new file mode 100644 index 00000000000000..786c21c9a8baaf --- /dev/null +++ b/lib/internal/test/policy.js @@ -0,0 +1,7 @@ +'use strict'; + +process.emitWarning( + 'These APIs are for internal testing only. Do not use them.', + 'internal/test/policy'); + +module.exports = { runInPrivilegedScope }; diff --git a/node.gyp b/node.gyp index 2090f10e5333f9..441b42c62c292f 100644 --- a/node.gyp +++ b/node.gyp @@ -220,6 +220,7 @@ 'lib/internal/source_map/source_map.js', 'lib/internal/source_map/source_map_cache.js', 'lib/internal/test/binding.js', + 'lib/internal/test/policy.js', 'lib/internal/timers.js', 'lib/internal/tls.js', 'lib/internal/trace_events_async_hooks.js', @@ -737,7 +738,6 @@ 'src/node_perf_common.h', 'src/node_platform.h', 'src/policy/policy.h', - 'src/policy/policy-inl.h', 'src/node_process.h', 'src/node_report.h', 'src/node_revert.h', diff --git a/src/env-inl.h b/src/env-inl.h index 4568b0a8aabf32..d8cd343e7e796d 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -29,7 +29,6 @@ #include "env.h" #include "node.h" #include "util-inl.h" -#include "policy/policy-inl.h" #include "uv.h" #include "v8.h" #include "node_perf_common.h" @@ -247,8 +246,12 @@ inline size_t Environment::async_callback_scope_depth() const { return async_callback_scope_depth_; } -policy::PrivilegedAccessContext* Environment::privileged_access_context() { - return &privileged_access_context_; +inline void Environment::set_in_privileged_scope(bool on) { + in_privileged_scope_ = on; +} + +inline bool Environment::in_privileged_scope() const { + return in_privileged_scope_; } inline void Environment::PushAsyncCallbackScope() { diff --git a/src/env.cc b/src/env.cc index 48dd2aaac506bd..7e89c2828f2003 100644 --- a/src/env.cc +++ b/src/env.cc @@ -313,7 +313,6 @@ Environment::Environment(IsolateData* isolate_data, ThreadId thread_id) : isolate_(isolate), isolate_data_(isolate_data), - privileged_access_context_(isolate_data->options()->per_env.get()), async_hooks_(isolate, MAYBE_FIELD_PTR(env_info, async_hooks)), immediate_info_(isolate, MAYBE_FIELD_PTR(env_info, immediate_info)), tick_info_(isolate, MAYBE_FIELD_PTR(env_info, tick_info)), diff --git a/src/env.h b/src/env.h index 7208ff6de920ec..865fe7efce5240 100644 --- a/src/env.h +++ b/src/env.h @@ -37,7 +37,6 @@ #include "node_main_instance.h" #include "node_options.h" #include "node_perf_common.h" -#include "policy/policy.h" #include "req_wrap.h" #include "util.h" #include "uv.h" @@ -1040,8 +1039,6 @@ class Environment : public MemoryRetainer { inline const std::vector& argv(); const std::string& exec_path() const; - inline policy::PrivilegedAccessContext* privileged_access_context(); - typedef void (*HandleCleanupCb)(Environment* env, uv_handle_t* handle, void* arg); @@ -1189,6 +1186,9 @@ class Environment : public MemoryRetainer { inline node_module* extra_linked_bindings_head(); inline const Mutex& extra_linked_bindings_mutex() const; + inline void set_in_privileged_scope(bool on = true); + inline bool in_privileged_scope() const; + inline bool filehandle_close_warning() const; inline void set_filehandle_close_warning(bool on); @@ -1403,7 +1403,6 @@ class Environment : public MemoryRetainer { std::list loaded_addons_; v8::Isolate* const isolate_; IsolateData* const isolate_data_; - policy::PrivilegedAccessContext privileged_access_context_; uv_timer_t timer_handle_; uv_check_t immediate_check_handle_; uv_idle_t immediate_idle_handle_; @@ -1425,6 +1424,8 @@ class Environment : public MemoryRetainer { size_t async_callback_scope_depth_ = 0; std::vector destroy_async_id_list_; + bool in_privileged_scope_ = false; + #if HAVE_INSPECTOR std::unique_ptr coverage_connection_; std::unique_ptr cpu_profiler_connection_; diff --git a/src/node.cc b/src/node.cc index af7b5616230554..2fae33be3cd62e 100644 --- a/src/node.cc +++ b/src/node.cc @@ -298,9 +298,7 @@ void Environment::InitializeDiagnostics() { bool Environment::BootstrapPrivilegedAccessContext() { Local run_in_privileged_scope; MaybeLocal maybe_run_in_privileged_scope = - Function::New( - context(), - policy::PrivilegedAccessContext::Run); + Function::New(context(), policy::RunInPrivilegedScope); if (!maybe_run_in_privileged_scope.ToLocal(&run_in_privileged_scope)) return false; set_run_in_privileged_scope(run_in_privileged_scope); @@ -818,6 +816,14 @@ int ProcessGlobalArgs(std::vector* args, } } + if (per_process::root_policy.Apply( + per_process::cli_options->policy_deny, + per_process::cli_options->policy_grant).IsNothing()) { + errors->emplace_back( + "invalid permissions passed to --policy-deny or --policy-grant"); + return 12; + } + if (per_process::cli_options->disable_proto != "delete" && per_process::cli_options->disable_proto != "throw" && per_process::cli_options->disable_proto != "") { diff --git a/src/node_options.cc b/src/node_options.cc index 746c324af36af4..afd3f792dc4925 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -322,14 +322,6 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::experimental_policy_integrity, kAllowedInEnvironment); Implies("--policy-integrity", "[has_policy_integrity_string]"); - AddOption("--policy-deny", - "denied permissions", - &EnvironmentOptions::policy_deny, - kAllowedInEnvironment); - AddOption("--policy-grant", - "granted permissions", - &EnvironmentOptions::policy_grant, - kAllowedInEnvironment); AddOption("--experimental-repl-await", "experimental await keyword support in REPL", &EnvironmentOptions::experimental_repl_await, @@ -712,6 +704,14 @@ PerProcessOptionsParser::PerProcessOptionsParser( "generate diagnostic report on fatal (internal) errors", &PerProcessOptions::report_on_fatalerror, kAllowedInEnvironment); + AddOption("--policy-deny", + "denied permissions", + &PerProcessOptions::policy_deny, + kAllowedInEnvironment); + AddOption("--policy-grant", + "granted permissions", + &PerProcessOptions::policy_grant, + kAllowedInEnvironment); #ifdef NODE_HAVE_I18N_SUPPORT AddOption("--icu-data-dir", diff --git a/src/node_options.h b/src/node_options.h index 112899350e1177..a37328d225bd93 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -177,9 +177,6 @@ class EnvironmentOptions : public Options { std::vector preload_modules; - std::string policy_deny; - std::string policy_grant; - std::vector user_argv; inline DebugOptions* get_debug_options() { return &debug_options_; } @@ -263,6 +260,9 @@ class PerProcessOptions : public Options { bool trace_sigint = false; std::vector cmdline; + std::string policy_grant; + std::string policy_deny; + inline PerIsolateOptions* get_per_isolate_options(); void CheckOptions(std::vector* errors) override; }; diff --git a/src/policy/policy-inl.h b/src/policy/policy-inl.h deleted file mode 100644 index a00c4d741a30bc..00000000000000 --- a/src/policy/policy-inl.h +++ /dev/null @@ -1,218 +0,0 @@ -#ifndef SRC_POLICY_POLICY_INL_H_ -#define SRC_POLICY_POLICY_INL_H_ - -#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS - -#include "policy/policy.h" -#include "env-inl.h" -#include "util-inl.h" -#include "node_errors.h" - -#include - -namespace node { -namespace policy { - -Policy::Policy(EnvironmentOptions* options) { - Apply(options->policy_deny, options->policy_grant); -} - -Policy::Policy( - Policy* basis, - const std::string& deny, - const std::string& grant) { - permissions_ = basis->permissions_; - Apply(deny, grant, ApplyFlags::kIgnoreSpecials); -} - -Policy::Policy( - Policy* basis, - const std::vector& deny, - const std::vector& grant) { - permissions_ = basis->permissions_; - Apply(deny, grant, ApplyFlags::kIgnoreSpecials); -} - -bool Policy::test(Permission permission) const { - return UNLIKELY(permissions_.test(static_cast(permission))); -} - -#define V(name, _, parent) \ - if (permission == Permission::k##parent) \ - SetRecursively(Permission::k##name, value); -void Policy::SetRecursively(Permission permission, bool value) { - if (permission != Permission::kPermissionsRoot) - permissions_[static_cast(permission)] = value; - PERMISSIONS(V) -} -#undef V - -void Policy::Grant(Permission permission) { - if (LIKELY(locked_)) - return; - if (UNLIKELY(permission == Permission::kPermissionsCount)) - return; - SetRecursively(permission, false); -} - -void Policy::Grant(std::string permission) { - Grant(PermissionFromName(permission)); -} - -void Policy::Deny(Permission permission) { - if (UNLIKELY(permission == Permission::kPermissionsCount)) - return; - SetRecursively(permission, true); -} - -void Policy::Deny(std::string permission) { - Deny(PermissionFromName(permission)); -} - -bool Policy::is_granted(Permission permission) const { - return LIKELY(permission != Permission::kPermissionsCount) && - LIKELY(permission != Permission::kPermissionsRoot) && - !test(permission); -} - -bool Policy::is_granted(std::string permission) const { - return is_granted(PermissionFromName(permission)); -} - -void Policy::Apply( - const std::string& deny, - const std::string& grant, - ApplyFlags flags) { - std::vector denied = Policy::Parse(deny); - std::vector granted = Policy::Parse(grant); - Apply(denied, granted); -} - -void Policy::Apply( - const std::vector& deny, - const std::vector& grant, - ApplyFlags flags) { - - // The entire special category is denied by default if there - // are any other denials (and the flag is set); - if (deny.size() > 0 && flags == ApplyFlags::kDenySpecials) - Deny(Permission::kSpecial); - - for (Permission permission : deny) - Deny(permission); - - for (Permission permission : grant) - Grant(permission); - Lock(); -} - -#define V(Name, label, _) \ - if (strcmp(name.c_str(), label) == 0) return Permission::k##Name; -Permission Policy::PermissionFromName(const std::string& name) { - if (strcmp(name.c_str(), "*") == 0) return Permission::kPermissionsRoot; - PERMISSIONS(V) - return Permission::kPermissionsCount; -} -#undef V - -std::vector Policy::Parse( - const std::string& list, - ParseStatus* status) { - std::vector permissions; - for (auto name : SplitString(list, ',')) { - Permission permission = PermissionFromName(name); - if (permission != Permission::kPermissionsCount) - permissions.push_back(permission); - else if (status != nullptr) - *status = ParseStatus::UNKNOWN; - } - return permissions; -} - -PrivilegedAccessContext::PrivilegedAccessContext( - EnvironmentOptions* options) { - std::unique_ptr policy = std::make_unique(options); - Push(std::move(policy)); -} - -// Push a new Policy onto the stack. The new policy starts as a -// clone of the current Policy with the additionally given -// denials/grants applied. -void PrivilegedAccessContext::Push( - const std::string& deny, - const std::string& grant) { - std::unique_ptr policy = - std::make_unique( - policy_stack_.front().get(), - deny, - grant); - Push(std::move(policy)); -} - -// Push a new Policy onto the stack. The new policy starts as a -// clone of the current Policy with the additionally given -// denials/grants applied. -void PrivilegedAccessContext::Push( - const std::vector& deny, - const std::vector& grant) { - std::unique_ptr policy = - std::make_unique( - policy_stack_.front().get(), - deny, - grant); - Push(std::move(policy)); -} - -void PrivilegedAccessContext::Push(std::unique_ptr policy) { - policy_stack_.push_front(std::move(policy)); -} - -bool PrivilegedAccessContext::Pop() { - // Can't remove the root Policy - if (policy_stack_.size() == 1) - return false; - policy_stack_.pop_front(); - return true; -} - -bool PrivilegedAccessContext::is_granted(Permission permission) { - return policy_stack_.front()->is_granted(permission); -} - -bool PrivilegedAccessContext::is_granted(const std::string& permission) { - return policy_stack_.front()->is_granted(permission); -} - -void PrivilegedAccessContext::Deny(Permission permission) { - policy_stack_.front()->Deny(permission); -} - -void PrivilegedAccessContext::Deny(const std::string& permission) { - policy_stack_.front()->Deny(permission); -} - -PrivilegedAccessContext::Scope::Scope( - Environment* env, - const std::string& deny, - const std::string& grant) - : env_(env) { - env_->privileged_access_context()->Push(deny, grant); -} - -PrivilegedAccessContext::Scope::Scope( - Environment* env, - const std::vector& deny, - const std::vector& grant) - : env_(env) { - env_->privileged_access_context()->Push(deny, grant); -} - -PrivilegedAccessContext::Scope::~Scope() { - env_->privileged_access_context()->Pop(); -} - -} // namespace policy -} // namespace node - -#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS -#endif // SRC_POLICY_POLICY_INL_H_ diff --git a/src/policy/policy.cc b/src/policy/policy.cc index 88d0de00f39a07..e31a824c134bcb 100644 --- a/src/policy/policy.cc +++ b/src/policy/policy.cc @@ -1,4 +1,4 @@ -#include "policy-inl.h" // NOLINT(build/include) +#include "policy.h" #include "aliased_struct-inl.h" #include "base_object-inl.h" #include "env-inl.h" @@ -13,19 +13,38 @@ namespace node { -using v8::Array; using v8::Context; using v8::Function; using v8::FunctionCallbackInfo; using v8::Int32; +using v8::Just; using v8::Local; -using v8::MaybeLocal; +using v8::Maybe; +using v8::Nothing; using v8::Object; using v8::Value; +namespace per_process { +// The root policy is establish at process start using +// the --policy-grant and --policy-deny command line +// arguments. Every node::Environment has it's own +// Policy that derives from the root. +policy::Policy root_policy; +} // namespace per_process + namespace policy { +PrivilegedScope::PrivilegedScope(Environment* env_) : env(env_) { + env->set_in_privileged_scope(true); +} + +PrivilegedScope::~PrivilegedScope() { + env->set_in_privileged_scope(false); +} + namespace { +Mutex apply_mutex_; + Permission GetPermission(Local arg) { if (!arg->IsInt32()) return Permission::kPermissionsCount; @@ -36,18 +55,15 @@ Permission GetPermission(Local arg) { } return static_cast(permission); } -} // namespace static void Deny(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK(args[0]->IsString()); Utf8Value list(env->isolate(), args[0]); - Policy::ParseStatus status = Policy::ParseStatus::OK; - auto permissions = Policy::Parse(*list, &status); - if (status != Policy::ParseStatus::OK) - return args.GetReturnValue().Set(static_cast(status)); - for (Permission permission : permissions) - env->privileged_access_context()->Deny(permission); + // If Apply returns Nothing, there was an error + // parsing the list, in which case we'll return undefined. + if (per_process::root_policy.Apply(*list).IsJust()) + return args.GetReturnValue().Set(true); } static void FastCheck(const FunctionCallbackInfo& args) { @@ -55,80 +71,121 @@ static void FastCheck(const FunctionCallbackInfo& args) { Permission permission = GetPermission(args[0]); CHECK_LT(permission, Permission::kPermissionsCount); CHECK_GT(permission, Permission::kPermissionsRoot); - args.GetReturnValue().Set( - env->privileged_access_context()->is_granted(GetPermission(args[0]))); + args.GetReturnValue().Set(Policy::is_granted(env, GetPermission(args[0]))); } static void Check(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK(args[0]->IsString()); Utf8Value list(env->isolate(), args[0]); - Policy::ParseStatus status = Policy::ParseStatus::OK; - auto permissions = Policy::Parse(*list, &status); - if (status != Policy::ParseStatus::OK) - return args.GetReturnValue().Set(static_cast(status)); - for (Permission permission : permissions) { - if (!env->privileged_access_context()->is_granted(permission)) - return args.GetReturnValue().Set(false); + Maybe permissions = Policy::Parse(*list); + // If permissions is empty, there was an error parsing. + // return undefined to indicate check failure. + if (permissions.IsNothing()) return; + args.GetReturnValue().Set(Policy::is_granted(env, permissions.FromJust())); +} + +#define V(name, _, parent) \ + if (permission == Permission::k##parent) \ + SetRecursively(set, Permission::k##name); +void SetRecursively(PermissionSet* set, Permission permission) { + if (permission != Permission::kPermissionsRoot) + set->set(static_cast(permission)); + PERMISSIONS(V) +} +#undef V + +} // namespace + +void RunInPrivilegedScope(const FunctionCallbackInfo& args) { + CHECK(args[0]->IsFunction()); // The function to execute + CHECK(!args.IsConstructCall()); + Local fn = args[0].As(); + + Environment* env = Environment::GetCurrent(args); + PrivilegedScope privileged_scope(env); + + SlicedArguments call_args(args, 1); + + Local ret; + if (fn->Call( + env->context(), + args.This(), + call_args.length(), + call_args.out()).ToLocal(&ret)) { + args.GetReturnValue().Set(ret); } - args.GetReturnValue().Set(true); } -namespace { -std::vector ToPermissions(Environment* env, Local input) { - std::vector permissions; - for (size_t n = 0; n < input->Length(); n++) { - Local val = input->Get(env->context(), n).FromMaybe(Local()); - CHECK(val->IsInt32()); - int32_t permission = val.As()->Value(); - CHECK_GE(permission, static_cast(Permission::kPermissionsRoot)); - CHECK_LT(permission, static_cast(Permission::kPermissionsCount)); - permissions.push_back(static_cast(permission)); +bool Policy::is_granted(Environment* env, Permission permission) { + return env->in_privileged_scope() + ? true + : per_process::root_policy.is_granted(permission); +} + +bool Policy::is_granted(Environment* env, std::string permission) { + return env->in_privileged_scope() + ? true + : per_process::root_policy.is_granted(permission); +} + +bool Policy::is_granted(Environment* env, const PermissionSet& permissions) { + return env->in_privileged_scope() + ? true + : per_process::root_policy.is_granted(permissions); +} + +Maybe Policy::Parse(const std::string& list) { + PermissionSet set; + for (const auto& name : SplitString(list, ',')) { + Permission permission = PermissionFromName(name); + if (permission == Permission::kPermissionsCount) + return Nothing(); + SetRecursively(&set, permission); } - return permissions; + return Just(set); } -} // namespace -// The starting arguments must consist of a function along with -// two strings identifying the permissions to deny or grant. -// The remaining arguments will be passed on to the function, -void PrivilegedAccessContext::Run( - const v8::FunctionCallbackInfo& args) { - CHECK_GE(args.Length(), 3); - CHECK(args[0]->IsString() || args[0]->IsArray()); // The denials - CHECK_IMPLIES(args[0]->IsString(), args[1]->IsString()); - CHECK_IMPLIES(args[1]->IsArray(), args[1]->IsArray()); - CHECK(args[2]->IsFunction()); // The function to execute +#define V(Name, label, _) \ + if (strcmp(name.c_str(), label) == 0) return Permission::k##Name; +Permission Policy::PermissionFromName(const std::string& name) { + if (strcmp(name.c_str(), "*") == 0) return Permission::kPermissionsRoot; + PERMISSIONS(V) + return Permission::kPermissionsCount; +} +#undef V + +Maybe Policy::Apply(const std::string& deny, const std::string& grant) { + Maybe deny_set = Parse(deny); + if (deny_set.IsNothing()) return Nothing(); + Maybe grant_set = Parse(grant); + if (grant_set.IsNothing()) return Nothing(); + Apply(deny_set.FromJust(), grant_set.FromJust()); + return Just(true);; +} - Local fn = args[2].As(); +void Policy::Apply(const PermissionSet& deny, const PermissionSet& grant) { + // permissions_ is an inverted set. If a bit is *set* in + // permissions_, then the permission is *denied*, otherwise + // it is granted. - CHECK(!args.IsConstructCall()); + // Just in case Deny is called from multiple Worker threads. + // TODO(@jasnell): Do we want to allow workers to call deny? + Mutex::ScopedLock lock(apply_mutex_); - SlicedArguments call_args(args, 3); - Environment* env = Environment::GetCurrent(args); - MaybeLocal ret; - - if (args[0]->IsArray()) { - std::vector deny = ToPermissions(env, args[0].As()); - std::vector grant = ToPermissions(env, args[1].As()); - PrivilegedAccessContext::Scope privileged_scope(env, deny, grant); - ret = fn->Call( - env->context(), - args.This(), - call_args.length(), - call_args.out()); - } else { - Utf8Value deny(env->isolate(), args[0]); - Utf8Value grant(env->isolate(), args[1]); - PrivilegedAccessContext::Scope privileged_scope(env, *deny, *grant); - ret = fn->Call( - env->context(), - args.This(), - call_args.length(), - call_args.out()); + if (deny.count() > 0) { +#define V(name, _, __) \ + permissions_.set(static_cast(Permission::k##name)); + SPECIAL_PERMISSIONS(V) +#undef V } - args.GetReturnValue().Set(ret.FromMaybe(Local())); + permissions_ |= deny; + + if (!locked_) + permissions_ &= ~grant; + + locked_ = true; } void Initialize(Local target, @@ -155,7 +212,7 @@ void RegisterExternalReferences( registry->Register(Deny); registry->Register(FastCheck); registry->Register(Check); - registry->Register(PrivilegedAccessContext::Run); + registry->Register(RunInPrivilegedScope); } } // namespace policy diff --git a/src/policy/policy.h b/src/policy/policy.h index 2cce7606eedbed..959eac14050353 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -9,17 +9,18 @@ #include #include #include -#include namespace node { + +class Environment; + namespace policy { // Special Permissions are denied by default if any other permission is denied. #define SPECIAL_PERMISSIONS(V) \ - V(Special, "special", PermissionsRoot) \ - V(SpecialInspector, "special.inspector", Special) \ - V(SpecialAddons, "special.addons", Special) \ - V(SpecialChildProcess, "special.child_process", Special) + V(SpecialInspector, "inspector", PermissionsRoot) \ + V(SpecialAddons, "addons", PermissionsRoot) \ + V(SpecialChildProcess, "child_process", PermissionsRoot) #define FILESYSTEM_PERMISSIONS(V) \ V(FileSystem, "fs", PermissionsRoot) \ @@ -28,17 +29,11 @@ namespace policy { #define NETWORKING_PERMISSIONS(V) \ V(Net, "net", PermissionsRoot) \ - V(NetUdp, "net.udp", Net) \ - V(NetDNS, "net.dns", Net) \ - V(NetTCP, "net.tcp", Net) \ - V(NetTCPIn, "net.tcp.in", NetTCP) \ - V(NetTCPOut, "net.tcp.out", NetTCP) \ - V(NetTLS, "net.tls", Net) \ - V(NetTLSLog, "net.tls.log", NetTLS) + V(NetIn, "net.in", Net) \ + V(NetOut, "net.out", Net) #define EXPERIMENTAL_PERMISSIONS(V) \ - V(Experimental, "experimental", PermissionsRoot) \ - V(ExperimentalWasi, "experimental.wasi", Experimental) + V(Experimental, "wasi", PermissionsRoot) \ #define PERMISSIONS(V) \ EXPERIMENTAL_PERMISSIONS(V) \ @@ -48,8 +43,9 @@ namespace policy { V(Process, "process", PermissionsRoot) \ V(Signal, "signal", PermissionsRoot) \ V(Timing, "timing", PermissionsRoot) \ - V(User, "user", PermissionsRoot) \ + V(Env, "env", PermissionsRoot) \ V(Workers, "workers", PermissionsRoot) \ + V(Policy, "policy", PermissionsRoot) #define V(name, _, __) k##name, enum class Permission { @@ -59,138 +55,81 @@ enum class Permission { }; #undef V -class Policy final { - public: - enum class ParseStatus { - OK, - UNKNOWN - }; +using PermissionSet = + std::bitset(Permission::kPermissionsCount)>; - static inline Permission PermissionFromName(const std::string& name); +void RunInPrivilegedScope( + const v8::FunctionCallbackInfo& args); - static inline std::vector Parse( - const std::string& list, - ParseStatus* status = nullptr); +class Policy final { + public: + static bool is_granted(Environment* env, Permission permission); + static bool is_granted(Environment* env, std::string permission); + static bool is_granted(Environment* env, const PermissionSet& permissions); - inline explicit Policy(EnvironmentOptions* options); + static Permission PermissionFromName(const std::string& name); - inline Policy( - Policy* basis, - const std::string& deny, - const std::string& grant); + static v8::Maybe Parse(const std::string& list); - inline Policy( - Policy* basis, - const std::vector& deny, - const std::vector& grant); + Policy() = default; Policy(Policy&& other) = delete; Policy& operator=(Policy&& other) = delete; Policy(const Policy& other) = delete; Policy& operator=(const Policy& other) = delete; - inline void Deny(Permission permission); - inline void Deny(std::string permission); + // Returns true after setting the permissions. If Nothing + // is returned, the permissions could not be parsed successfully. + v8::Maybe Apply( + const std::string& deny, + const std::string& grant = std::string()); - inline void Grant(Permission permission); - inline void Grant(std::string permission); + inline bool is_granted(Permission permission) const { + return LIKELY(permission != Permission::kPermissionsCount) && + LIKELY(permission != Permission::kPermissionsRoot) && + !test(permission); + } - // Once a policy is locked, no additional grants will be permitted. - inline void Lock() { locked_ = true; } + inline bool is_granted(std::string permission) const { + return is_granted(PermissionFromName(permission)); + } - inline bool is_granted(Permission permission) const; - inline bool is_granted(std::string permission) const; + inline bool is_granted(const PermissionSet& set) const { + PermissionSet check = permissions_; + check &= set; + return check.none(); + } // Once a policy is locked, no additional grants will be permitted. inline bool is_locked() const { return locked_; } private: - inline void MaybeDenySpecials(Policy* policy, Permission permission); - - enum class ApplyFlags { - // When used, instructs Apply not to deny specials automatically - kIgnoreSpecials, - // When used, instructs Apply to deny specials if there are any - // other denials - kDenySpecials - }; - - inline void Apply( - const std::string& deny, - const std::string& grant, - ApplyFlags flags = ApplyFlags::kDenySpecials); - - inline void Apply( - const std::vector& deny, - const std::vector& grant, - ApplyFlags flags = ApplyFlags::kDenySpecials); + // Returns true after setting the permissions. If Nothing + // is returned, the permissions could not be + void Apply(const PermissionSet& deny, const PermissionSet& grant); - inline bool test(Permission permission) const; - inline void SetRecursively(Permission permission, bool value); + inline bool test(Permission permission) const { + return UNLIKELY(permissions_.test(static_cast(permission))); + } bool locked_ = false; - std::bitset(Permission::kPermissionsCount)> permissions_; + PermissionSet permissions_; }; -// The Environment always has exactly one PriviledgeAccessContext that covers -// the currently active Policy. Think of the PrivilegedAccessContext as a -// stack of Policy objects with the default or root policy as the base. -class PrivilegedAccessContext { - public: - class Scope { - public: - inline Scope( - Environment* env, - const std::string& deny, - const std::string& grant); - - inline Scope( - Environment* env, - const std::vector& deny, - const std::vector& grant); - - inline ~Scope(); - private: - Environment* env_; - }; - - static void Run(const v8::FunctionCallbackInfo& args); - - inline explicit PrivilegedAccessContext(EnvironmentOptions* options); - - PrivilegedAccessContext( - PrivilegedAccessContext&& other) = delete; - PrivilegedAccessContext& operator=( - PrivilegedAccessContext&& other) = delete; - PrivilegedAccessContext( - const PrivilegedAccessContext& other) = delete; - PrivilegedAccessContext& operator=( - const PrivilegedAccessContext& other) = delete; - - inline void Push( - const std::string& deny, - const std::string& grant); - - inline void Push( - const std::vector& deny, - const std::vector& grant); - - inline bool Pop(); - - inline bool is_granted(Permission permission); - - inline bool is_granted(const std::string& permission); - - inline void Deny(Permission permission); - - inline void Deny(const std::string& permission); - - private: - inline void Push(std::unique_ptr policy); - std::deque> policy_stack_; +// When code is running with the privileged scope, policy +// permission checking should be disabled. +struct PrivilegedScope { + Environment* env; + explicit PrivilegedScope(Environment* env_); + ~PrivilegedScope(); }; } // namespace policy + +namespace per_process { +extern policy::Policy root_policy; +} // namespace per_process + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS From 10572b6f9bac74aa21ccd9bf049ce759233a82bf Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 11 Dec 2020 14:50:35 -0800 Subject: [PATCH 3/3] [WIP] make PrivilegeScope re-entrant --- src/env-inl.h | 9 +++++++-- src/env.h | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/env-inl.h b/src/env-inl.h index d8cd343e7e796d..ab1a74e8d012da 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -247,11 +247,16 @@ inline size_t Environment::async_callback_scope_depth() const { } inline void Environment::set_in_privileged_scope(bool on) { - in_privileged_scope_ = on; + if (on) + in_privileged_scope_++; + else { + CHECK_GT(in_privileged_scope_, 0); + in_privileged_scope_--; + } } inline bool Environment::in_privileged_scope() const { - return in_privileged_scope_; + return in_privileged_scope_ > 0; } inline void Environment::PushAsyncCallbackScope() { diff --git a/src/env.h b/src/env.h index 865fe7efce5240..1b3e52209d2f7e 100644 --- a/src/env.h +++ b/src/env.h @@ -1424,7 +1424,7 @@ class Environment : public MemoryRetainer { size_t async_callback_scope_depth_ = 0; std::vector destroy_async_id_list_; - bool in_privileged_scope_ = false; + size_t in_privileged_scope_ = 0; #if HAVE_INSPECTOR std::unique_ptr coverage_connection_;