diff --git a/lib/internal/bootstrap/pre_execution.js b/lib/internal/bootstrap/pre_execution.js index 1871409c86eb76..c6e94fb8790b78 100644 --- a/lib/internal/bootstrap/pre_execution.js +++ b/lib/internal/bootstrap/pre_execution.js @@ -452,7 +452,7 @@ function initializeClusterIPC() { function initializePolicy() { const experimentalPolicy = getOptionValue('--experimental-policy'); - const { setup, check, deny } = require('internal/process/policy') + const { setup, check, deny } = require('internal/process/policy'); if (experimentalPolicy) { process.emitWarning('Policies are experimental.', 'ExperimentalWarning'); diff --git a/lib/internal/process/policy.js b/lib/internal/process/policy.js index 96c06ffda5ba1f..84470f039f5938 100644 --- a/lib/internal/process/policy.js +++ b/lib/internal/process/policy.js @@ -62,16 +62,16 @@ module.exports = ObjectFreeze({ this.manifest.assertIntegrity(moduleURL, content); }, - deny(permission) { + deny(permission, params) { if (typeof permission !== 'string') throw new ERR_INVALID_ARG_TYPE('permission', 'string', permission); - return policy.deny(permission); + return policy.deny(permission, params); }, - check(permission) { + check(permission, params) { if (typeof permission !== 'string') throw new ERR_INVALID_ARG_TYPE('permission', 'string', permission); - return policy.check(permission); + return policy.check(permission, params); } }); diff --git a/node.gyp b/node.gyp index 5c8dd9768a9d30..6eab87258b85e0 100644 --- a/node.gyp +++ b/node.gyp @@ -543,6 +543,7 @@ 'src/string_bytes.cc', 'src/string_decoder.cc', 'src/policy/policy.cc', + 'src/policy/policy_deny_fs.cc', 'src/tcp_wrap.cc', 'src/timers.cc', 'src/timer_wrap.cc', @@ -637,6 +638,8 @@ 'src/node_worker.h', 'src/pipe_wrap.h', 'src/policy/policy.h', + 'src/policy/policy_deny.h', + 'src/policy/policy_deny_fs.h', 'src/req_wrap.h', 'src/req_wrap-inl.h', 'src/spawn_sync.h', diff --git a/src/fs_event_wrap.cc b/src/fs_event_wrap.cc index 9467dbb2eca20f..e373b681e89e86 100644 --- a/src/fs_event_wrap.cc +++ b/src/fs_event_wrap.cc @@ -135,7 +135,6 @@ void FSEventWrap::New(const FunctionCallbackInfo& args) { // wrap.start(filename, persistent, recursive, encoding) void FSEventWrap::Start(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); FSEventWrap* wrap = Unwrap(args.This()); CHECK_NOT_NULL(wrap); @@ -146,6 +145,8 @@ void FSEventWrap::Start(const FunctionCallbackInfo& args) { BufferValue path(env->isolate(), args[0]); CHECK_NOT_NULL(*path); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemIn, *path); unsigned int flags = 0; if (args[2]->IsTrue()) diff --git a/src/node.cc b/src/node.cc index 2b413d73fa63b8..4212bd8b7025a5 100644 --- a/src/node.cc +++ b/src/node.cc @@ -859,9 +859,10 @@ int ProcessGlobalArgs(std::vector* args, if (v8_args_as_char_ptr.size() > 1) return 9; if (policy::root_policy.Apply( - per_process::cli_options->policy_deny).IsNothing()) { + per_process::cli_options->policy_deny_fs, + policy::Permission::kFileSystem).IsNothing()) { errors->emplace_back( - "invalid permissions passed to --policy-deny"); + "invalid permissions passed to --policy-deny-fs"); return 12; } diff --git a/src/node_file.cc b/src/node_file.cc index ed95ffcbad0841..909f09d9f43929 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -435,9 +435,6 @@ FileHandleReadWrap::FileHandleReadWrap(FileHandle* handle, Local obj) int FileHandle::ReadStart() { if (!IsAlive() || IsClosing()) return UV_EOF; - if (!node::policy::root_policy.is_granted( - node::policy::Permission::kFileSystemIn)) - return UV_EPERM; reading_ = true; @@ -862,7 +859,6 @@ void AfterScanDirWithTypes(uv_fs_t* req) { void Access(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); Isolate* isolate = env->isolate(); HandleScope scope(isolate); @@ -875,6 +871,8 @@ void Access(const FunctionCallbackInfo& args) { BufferValue path(isolate, args[0]); CHECK_NOT_NULL(*path); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemIn, *path); FSReqBase* req_wrap_async = GetReqWrap(args, 2); if (req_wrap_async != nullptr) { // access(path, mode, req) @@ -917,12 +915,13 @@ void Close(const FunctionCallbackInfo& args) { // Used to speed up module loading. Returns an array [string, boolean] static void InternalModuleReadJSON(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); Isolate* isolate = env->isolate(); uv_loop_t* loop = env->event_loop(); CHECK(args[0]->IsString()); node::Utf8Value path(isolate, args[0]); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemIn, path.ToString()); if (strlen(*path) != path.length()) { args.GetReturnValue().Set(Array::New(isolate)); @@ -1016,10 +1015,11 @@ static void InternalModuleReadJSON(const FunctionCallbackInfo& args) { // The speedup comes from not creating thousands of Stat and Error objects. static void InternalModuleStat(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); CHECK(args[0]->IsString()); node::Utf8Value path(env->isolate(), args[0]); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemIn, path.ToString()); uv_fs_t req; int rc = uv_fs_stat(env->event_loop(), &req, *path, nullptr); @@ -1035,13 +1035,14 @@ static void InternalModuleStat(const FunctionCallbackInfo& args) { static void Stat(const FunctionCallbackInfo& args) { BindingData* binding_data = Environment::GetBindingData(args); Environment* env = binding_data->env(); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); const int argc = args.Length(); CHECK_GE(argc, 2); BufferValue path(env->isolate(), args[0]); CHECK_NOT_NULL(*path); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemIn, *path); bool use_bigint = args[1]->IsTrue(); FSReqBase* req_wrap_async = GetReqWrap(args, 2, use_bigint); @@ -1067,13 +1068,14 @@ static void Stat(const FunctionCallbackInfo& args) { static void LStat(const FunctionCallbackInfo& args) { BindingData* binding_data = Environment::GetBindingData(args); Environment* env = binding_data->env(); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); const int argc = args.Length(); CHECK_GE(argc, 3); BufferValue path(env->isolate(), args[0]); CHECK_NOT_NULL(*path); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemIn, *path); bool use_bigint = args[1]->IsTrue(); FSReqBase* req_wrap_async = GetReqWrap(args, 2, use_bigint); @@ -1100,7 +1102,6 @@ static void LStat(const FunctionCallbackInfo& args) { static void FStat(const FunctionCallbackInfo& args) { BindingData* binding_data = Environment::GetBindingData(args); Environment* env = binding_data->env(); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); const int argc = args.Length(); CHECK_GE(argc, 2); @@ -1187,7 +1188,6 @@ static void Link(const FunctionCallbackInfo& args) { static void ReadLink(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); Isolate* isolate = env->isolate(); const int argc = args.Length(); @@ -1195,6 +1195,8 @@ static void ReadLink(const FunctionCallbackInfo& args) { BufferValue path(isolate, args[0]); CHECK_NOT_NULL(*path); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemIn, *path); const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8); @@ -1604,7 +1606,6 @@ static void MKDir(const FunctionCallbackInfo& args) { static void RealPath(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); Isolate* isolate = env->isolate(); const int argc = args.Length(); @@ -1612,6 +1613,8 @@ static void RealPath(const FunctionCallbackInfo& args) { BufferValue path(isolate, args[0]); CHECK_NOT_NULL(*path); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemIn, *path); const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8); @@ -1649,7 +1652,6 @@ static void RealPath(const FunctionCallbackInfo& args) { static void ReadDir(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); Isolate* isolate = env->isolate(); const int argc = args.Length(); @@ -1657,6 +1659,8 @@ static void ReadDir(const FunctionCallbackInfo& args) { BufferValue path(isolate, args[0]); CHECK_NOT_NULL(*path); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemIn, *path); const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8); @@ -1737,13 +1741,15 @@ static void ReadDir(const FunctionCallbackInfo& args) { static void Open(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); const int argc = args.Length(); CHECK_GE(argc, 3); BufferValue path(env->isolate(), args[0]); CHECK_NOT_NULL(*path); + // Open can be called either in write or read + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystem, *path); CHECK(args[1]->IsInt32()); const int flags = args[1].As()->Value(); @@ -1771,7 +1777,6 @@ static void Open(const FunctionCallbackInfo& args) { static void OpenFileHandle(const FunctionCallbackInfo& args) { BindingData* binding_data = Environment::GetBindingData(args); Environment* env = binding_data->env(); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); Isolate* isolate = env->isolate(); const int argc = args.Length(); @@ -1779,6 +1784,8 @@ static void OpenFileHandle(const FunctionCallbackInfo& args) { BufferValue path(isolate, args[0]); CHECK_NOT_NULL(*path); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemIn, *path); CHECK(args[1]->IsInt32()); const int flags = args[1].As()->Value(); @@ -1855,6 +1862,8 @@ static void WriteBuffer(const FunctionCallbackInfo& args) { CHECK(args[0]->IsInt32()); const int fd = args[0].As()->Value(); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemOut, fd); CHECK(Buffer::HasInstance(args[1])); Local buffer_obj = args[1].As(); @@ -1953,9 +1962,10 @@ static void WriteString(const FunctionCallbackInfo& args) { const int argc = args.Length(); CHECK_GE(argc, 4); - CHECK(args[0]->IsInt32()); const int fd = args[0].As()->Value(); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemOut, fd); const int64_t pos = GetOffset(args[2]); @@ -2051,13 +2061,14 @@ static void WriteString(const FunctionCallbackInfo& args) { */ static void Read(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - THROW_IF_INSUFFICIENT_PERMISSIONS(env, policy::Permission::kFileSystemIn); const int argc = args.Length(); CHECK_GE(argc, 5); CHECK(args[0]->IsInt32()); const int fd = args[0].As()->Value(); + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, policy::Permission::kFileSystemIn, fd); CHECK(Buffer::HasInstance(args[1])); Local buffer_obj = args[1].As(); diff --git a/src/node_options.cc b/src/node_options.cc index b24e3d096d8b04..fcf57e277715f5 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -859,9 +859,9 @@ PerProcessOptionsParser::PerProcessOptionsParser( "force FIPS crypto (cannot be disabled)", &PerProcessOptions::force_fips_crypto, kAllowedInEnvironment); - AddOption("--policy-deny", - "denied permissions", - &PerProcessOptions::policy_deny, + AddOption("--policy-deny-fs", + "denied permissions to the filesystem", + &PerProcessOptions::policy_deny_fs, kAllowedInEnvironment); AddOption("--secure-heap", "total size of the OpenSSL secure heap", diff --git a/src/node_options.h b/src/node_options.h index 74c2cd9d3d0e22..e268cab57107d0 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -244,7 +244,7 @@ class PerProcessOptions : public Options { bool print_v8_help = false; bool print_version = false; - std::string policy_deny; + std::string policy_deny_fs; #ifdef NODE_HAVE_I18N_SUPPORT std::string icu_data_dir; diff --git a/src/policy/policy.cc b/src/policy/policy.cc index d612181d059f6e..5ebcf638e73d3b 100644 --- a/src/policy/policy.cc +++ b/src/policy/policy.cc @@ -14,67 +14,91 @@ namespace node { +using v8::Array; using v8::Context; using v8::FunctionCallbackInfo; +using v8::Integer; +using v8::Just; using v8::Local; using v8::Maybe; -using v8::Object; -using v8::Value; using v8::Nothing; -using v8::Just; +using v8::Object; using v8::String; +using v8::Value; namespace policy { // The root policy is establish at process start using -// the --policy-grant and --policy-deny command line -// arguments. +// the --policy-deny-* command line arguments. Policy root_policy; namespace { +// policy.deny('fs.in', ['/tmp/']) +// policy.deny('fs.in') static void Deny(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + v8::Isolate* isolate = env->isolate(); + CHECK(args[0]->IsString()); - Utf8Value list(env->isolate(), args[0]); - if (root_policy.Apply(*list).IsJust()) - return args.GetReturnValue().Set(true); + CHECK(args.Length() >= 2 || args[1]->IsArray()); + + std::string denyScope = *String::Utf8Value(isolate, args[0]); + Permission scope = Policy::StringToPermission(denyScope); + if (scope == Permission::kPermissionsRoot) { + return args.GetReturnValue().Set(false); + } + + Local jsParams = Local::Cast(args[1]); + std::vector params; + for (uint32_t i = 0; i < jsParams->Length(); ++i) { + Local arg( + jsParams + ->Get(isolate->GetCurrentContext(), Integer::New(isolate, i)) + .ToLocalChecked()); + + String::Utf8Value utf8_arg(isolate, arg); + params.push_back(*utf8_arg); + } + + return args.GetReturnValue() + .Set(root_policy.Deny(scope, params)); } +// policy.check('fs.in', '/tmp/') +// policy.check('fs.in') static void Check(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + v8::Isolate* isolate = env->isolate(); CHECK(args[0]->IsString()); - const std::string permString = *String::Utf8Value(env->isolate(), args[0]); - args.GetReturnValue() - .Set(root_policy.is_granted(permString)); -} + CHECK(args.Length() >= 2 || args[1]->IsString()); -#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) + std::string denyScope = *String::Utf8Value(isolate, args[0]); + Permission scope = Policy::StringToPermission(denyScope); + if (scope == Permission::kPermissionsRoot) { + // TODO(rafaelgss): throw? + return args.GetReturnValue().Set(false); + } + + return args.GetReturnValue() + .Set(root_policy.is_granted(scope, *String::Utf8Value(isolate, args[1]))); } -#undef V } // namespace #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; + if (perm == Permission::k##Name) return #Name; +const char* Policy::PermissionToString(const Permission perm) { PERMISSIONS(V) - return Permission::kPermissionsCount; + return nullptr; } #undef V #define V(Name, label, _) \ - if (perm == Permission::k##Name) return #Name; -const char* Policy::PermissionToString(const Permission perm) { + if (perm == label) return Permission::k##Name; +Permission Policy::StringToPermission(std::string perm) { PERMISSIONS(V) - return nullptr; + return Permission::kPermissionsRoot; } #undef V @@ -91,27 +115,20 @@ void Policy::ThrowAccessDenied(Environment* env, Permission perm) { env->isolate()->ThrowException(err); } -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); +Maybe Policy::Apply(const std::string& deny, Permission scope) { + auto policy = deny_policies.find(scope); + if (policy != deny_policies.end()) { + return policy->second->Apply(deny); } - return Just(set); + return Just(false); } -Maybe Policy::Apply(const std::string& deny) { - Maybe deny_set = Parse(deny); - - if (deny_set.IsNothing()) return Nothing(); - Apply(deny_set.FromJust()); - return Just(true); -} - -void Policy::Apply(const PermissionSet& deny) { - permissions_ |= deny; +bool Policy::Deny(Permission scope, std::vector params) { + auto policy = deny_policies.find(scope); + if (policy != deny_policies.end()) { + return policy->second->Deny(scope, params); + } + return false; } void Initialize(Local target, diff --git a/src/policy/policy.h b/src/policy/policy.h index 2faf5dc02ef0da..675bf7942d631f 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -5,8 +5,10 @@ #include "node_options.h" #include "v8.h" +#include "policy/policy_deny.h" +#include "policy/policy_deny_fs.h" -#include +#include #include namespace node { @@ -15,51 +17,53 @@ class Environment; namespace policy { -#define FILESYSTEM_PERMISSIONS(V) \ - V(FileSystem, "fs", PermissionsRoot) \ - V(FileSystemIn, "fs.in", FileSystem) \ - V(FileSystemOut, "fs.out", FileSystem) - -#define PERMISSIONS(V) \ - FILESYSTEM_PERMISSIONS(V) \ - -#define V(name, _, __) k##name, -enum class Permission { - kPermissionsRoot = -1, - PERMISSIONS(V) - kPermissionsCount -}; +#define THROW_IF_INSUFFICIENT_PERMISSIONS(env, perm_, resource_, ...) \ + if (!node::policy::root_policy.is_granted(perm_, resource_)) { \ + node::policy::Policy::ThrowAccessDenied((env), perm_); \ + } + +class Policy { + public: + // TODO(rafaelgss): release pointers + Policy() { + auto denyFs = new PolicyDenyFs(); +#define V(Name, _, __) \ + deny_policies.insert(std::make_pair(Permission::k##Name, denyFs)); + FILESYSTEM_PERMISSIONS(V) #undef V - -#define THROW_IF_INSUFFICIENT_PERMISSIONS(env, perm_, ...) \ - if (!node::policy::root_policy.is_granted(perm_)) { \ - node::policy::Policy::ThrowAccessDenied((env), perm_); \ + } + + inline bool is_granted(const Permission permission, const char* res) { + auto policy = deny_policies.find(permission); + if (policy != deny_policies.end()) { + return policy->second->is_granted(permission, res); + } + return false; } -using PermissionSet = - std::bitset(Permission::kPermissionsCount)>; - -class Policy final { - public: - inline bool is_granted(const Permission perm) const { - return !LIKELY(permissions_.test(static_cast(perm))); + inline bool is_granted(const Permission permission, std::string res) { + return is_granted(permission, res.c_str()); } - inline bool is_granted(const std::string& perm) const { - return is_granted(Policy::PermissionFromName(perm)); + inline bool is_granted(const Permission permission, unsigned fd) { + auto policy = deny_policies.find(permission); + if (policy != deny_policies.end()) { + return policy->second->is_granted(permission, fd); + } + return false; } - static Permission PermissionFromName(const std::string& name); + static Permission StringToPermission(std::string perm); static const char* PermissionToString(Permission perm); - static void ThrowAccessDenied(Environment* env, Permission perm); - v8::Maybe Parse(const std::string& list); - v8::Maybe Apply(const std::string& deny); - private: - void Apply(const PermissionSet& deny); + // CLI Call + v8::Maybe Apply(const std::string& deny, Permission scope); + // Policy.Deny API + bool Deny(Permission scope, std::vector params); - PermissionSet permissions_; + private: + std::map deny_policies; }; extern policy::Policy root_policy; diff --git a/src/policy/policy_deny.h b/src/policy/policy_deny.h new file mode 100644 index 00000000000000..ba9f0f3f2869a7 --- /dev/null +++ b/src/policy/policy_deny.h @@ -0,0 +1,45 @@ +#ifndef SRC_POLICY_POLICY_DENY_H_ +#define SRC_POLICY_POLICY_DENY_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "v8.h" +#include +#include + +namespace node { + +namespace policy { + +#define FILESYSTEM_PERMISSIONS(V) \ + V(FileSystem, "fs", PermissionsRoot) \ + V(FileSystemIn, "fs.in", FileSystem) \ + V(FileSystemOut, "fs.out", FileSystem) + +#define PERMISSIONS(V) \ + FILESYSTEM_PERMISSIONS(V) \ + +#define V(name, _, __) k##name, +enum class Permission { + kPermissionsRoot = -1, + PERMISSIONS(V) + kPermissionsCount +}; +#undef V + +class PolicyDeny { + public: + virtual v8::Maybe Apply(const std::string& deny) = 0; + virtual bool Deny(Permission scope, std::vector params) = 0; + virtual bool is_granted(Permission perm, const std::string& param = "") = 0; + virtual bool is_granted(Permission perm, unsigned fd) { + return false; + }; +}; + +} // namespace policy + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_POLICY_POLICY_DENY_H_ diff --git a/src/policy/policy_deny_fs.cc b/src/policy/policy_deny_fs.cc new file mode 100644 index 00000000000000..5e15d7dc5d5ef9 --- /dev/null +++ b/src/policy/policy_deny_fs.cc @@ -0,0 +1,142 @@ +#include "policy_deny_fs.h" +#include "base_object-inl.h" +#include "v8.h" + +#include +#include +#include +#include +#include +#include +#include + +using v8::Just; +using v8::Maybe; + +namespace node { + +namespace policy { + +// deny = 'fs' +// deny = 'in:/tmp/' +// deny = 'in:/tmp/,out:./example.js' +Maybe PolicyDenyFs::Apply(const std::string& deny) { + for (const auto& name : SplitString(deny, ',')) { + Permission perm = Permission::kPermissionsRoot; + for (std::string& opt : SplitString(name, ':')) { + if (perm == Permission::kPermissionsRoot) { + if (opt == "fs") { + deny_all_in_ = true; + deny_all_out_ = true; + return Just(true); + } + if (opt == "in") { + perm = Permission::kFileSystemIn; + deny_all_in_ = true; + } else if (opt == "out") { + perm = Permission::kFileSystemOut; + deny_all_out_ = true; + } else { + return Just(false); + } + } else { + RestrictAccess(perm, opt); + } + } + } + + return Just(true); +} + +bool PolicyDenyFs::Deny(Permission perm, std::vector params) { + if (perm == Permission::kFileSystem) { + deny_all_in_ = true; + deny_all_out_ = true; + return true; + } + + bool deny_all = params.size() == 0; + if (perm == Permission::kFileSystemIn) { + if (deny_all) deny_all_in_ = true; + // when deny_all_in is already true policy.deny should be idempotent + if (deny_all_in_) return true; + + RestrictAccess(perm, params); + return true; + } + + if (perm == Permission::kFileSystemOut) { + if (deny_all) deny_all_out_ = true; + // when deny_all_out is already true policy.deny should be idempotent + if (deny_all_out_) return true; + + RestrictAccess(perm, params); + return true; + } + + return false; +} + +void PolicyDenyFs::RestrictAccess(Permission perm, const std::string& res) { + char resolvedPath[PATH_MAX]; + // check the result + realpath(res.c_str(), resolvedPath); + + std::filesystem::path path(resolvedPath); + bool isDir = std::filesystem::is_directory(path); + // when there are parameters deny_params_ is automatically + // set to false + if (perm == Permission::kFileSystemIn) { + deny_all_in_ = false; + deny_in_params_.push_back(std::make_pair(resolvedPath, isDir)); + } else if (perm == Permission::kFileSystemOut) { + deny_all_out_ = false; + deny_out_params_.push_back(std::make_pair(resolvedPath, isDir)); + } +} + +void PolicyDenyFs::RestrictAccess(Permission perm, + std::vector params) { + for (auto& param : params) { + RestrictAccess(perm, param); + } +} + +bool PolicyDenyFs::is_granted(Permission perm, const std::string& param = "") { + switch (perm) { + case Permission::kFileSystem: + return !(deny_all_in_ && deny_all_out_); + case Permission::kFileSystemIn: + return !deny_all_in_ && + (param.empty() || PolicyDenyFs::is_granted(deny_in_params_, param)); + case Permission::kFileSystemOut: + return !deny_all_out_ && + (param.empty() || PolicyDenyFs::is_granted(deny_out_params_, param)); + default: + return false; + } +} + +bool PolicyDenyFs::is_granted(Permission perm, unsigned fd) { + // TODO(rafaelgss): FD to Filename + return true; +} + +bool PolicyDenyFs::is_granted(DenyFsParams params, const std::string& opt) { + char resolvedPath[PATH_MAX]; + realpath(opt.c_str(), resolvedPath); + for (auto& param : params) { + // is folder + if (param.second) { + if (strstr(resolvedPath, param.first.c_str()) == resolvedPath) { + return false; + } + } else if (param.first == resolvedPath) { + return false; + } + } + return true; +} + +} // namespace policy +} // namespace node diff --git a/src/policy/policy_deny_fs.h b/src/policy/policy_deny_fs.h new file mode 100644 index 00000000000000..402cd5642366f2 --- /dev/null +++ b/src/policy/policy_deny_fs.h @@ -0,0 +1,42 @@ +#ifndef SRC_POLICY_POLICY_DENY_FS_H_ +#define SRC_POLICY_POLICY_DENY_FS_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "v8.h" + +#include "policy/policy_deny.h" +#include + +using v8::Maybe; + +namespace node { + +namespace policy { + +using DenyFsParams = std::vector>; + +// TODO(rafaelgss): implement radix-tree algorithm +class PolicyDenyFs : public PolicyDeny { + public: + Maybe Apply(const std::string& deny); + bool Deny(Permission scope, std::vector params); + bool is_granted(Permission perm, const std::string& param); + bool is_granted(Permission perm, unsigned fd); + private: + static bool is_granted(DenyFsParams params, const std::string& opt); + void RestrictAccess(Permission scope, const std::string& param); + void RestrictAccess(Permission scope, std::vector params); + + DenyFsParams deny_in_params_; + DenyFsParams deny_out_params_; + bool deny_all_in_; + bool deny_all_out_; +}; + +} // namespace policy + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_POLICY_POLICY_DENY_FS_H_ diff --git a/test/fixtures/policy/deny/check.js b/test/fixtures/policy/deny/check.js deleted file mode 100644 index ed3a6ff25ed53c..00000000000000 --- a/test/fixtures/policy/deny/check.js +++ /dev/null @@ -1,7 +0,0 @@ -const assert = require('assert') - -assert(process.policy.check) - -console.log(process.policy.check('fs')) -console.log(process.policy.check('fs.in')) -console.log(process.policy.check('fs.out')) diff --git a/test/fixtures/policy/deny/fs/read-file.js b/test/fixtures/policy/deny/fs/read-file.js deleted file mode 100644 index 9ba058ccb5e20d..00000000000000 --- a/test/fixtures/policy/deny/fs/read-file.js +++ /dev/null @@ -1,4 +0,0 @@ -const fs = require('fs'); - -const data = fs.readFileSync(__filename, { encoding: 'utf8' }); -console.log(data); diff --git a/test/fixtures/policy/deny/protected-file.md b/test/fixtures/policy/deny/protected-file.md new file mode 100644 index 00000000000000..845763d2403ff4 --- /dev/null +++ b/test/fixtures/policy/deny/protected-file.md @@ -0,0 +1,3 @@ +# Protected File + +Example of a protected file to be used in the PolicyDenyFs module diff --git a/test/fixtures/policy/deny/protected-folder/protected-file.md b/test/fixtures/policy/deny/protected-folder/protected-file.md new file mode 100644 index 00000000000000..845763d2403ff4 --- /dev/null +++ b/test/fixtures/policy/deny/protected-folder/protected-file.md @@ -0,0 +1,3 @@ +# Protected File + +Example of a protected file to be used in the PolicyDenyFs module diff --git a/test/fixtures/policy/deny/regular-file.md b/test/fixtures/policy/deny/regular-file.md new file mode 100644 index 00000000000000..eb6054485b6fb4 --- /dev/null +++ b/test/fixtures/policy/deny/regular-file.md @@ -0,0 +1,3 @@ +# Regular File + +Example of a regular file to be used in the PolicyDenyFs module diff --git a/test/parallel/test-cli-policy-deny.js b/test/parallel/test-cli-policy-deny-fs.js similarity index 51% rename from test/parallel/test-cli-policy-deny.js rename to test/parallel/test-cli-policy-deny-fs.js index 9f1d7df3357669..2d3fc13ab45993 100644 --- a/test/parallel/test-cli-policy-deny.js +++ b/test/parallel/test-cli-policy-deny-fs.js @@ -7,7 +7,7 @@ if (!common.hasCrypto) const fixtures = require('../common/fixtures'); const { spawnSync } = require('child_process'); -const assert = require('assert') +const assert = require('assert'); const dep = fixtures.path('policy', 'deny', 'check.js'); @@ -15,7 +15,10 @@ const dep = fixtures.path('policy', 'deny', 'check.js'); const { status, stdout } = spawnSync( process.execPath, [ - '--policy-deny', 'fs', dep + '--policy-deny-fs', 'fs', '-e', + `console.log(process.policy.check("fs")); + console.log(process.policy.check("fs.in")); + console.log(process.policy.check("fs.out"));` ] ); @@ -30,7 +33,11 @@ const dep = fixtures.path('policy', 'deny', 'check.js'); const { status, stdout } = spawnSync( process.execPath, [ - '--policy-deny', 'fs.in', dep + + '--policy-deny-fs', 'in', '-e', + `console.log(process.policy.check("fs")); + console.log(process.policy.check("fs.in")); + console.log(process.policy.check("fs.out"));` ] ); @@ -45,7 +52,10 @@ const dep = fixtures.path('policy', 'deny', 'check.js'); const { status, stdout } = spawnSync( process.execPath, [ - '--policy-deny', 'fs.out', dep + '--policy-deny-fs', 'out', '-e', + `console.log(process.policy.check("fs")); + console.log(process.policy.check("fs.in")); + console.log(process.policy.check("fs.out"));` ] ); @@ -60,12 +70,15 @@ const dep = fixtures.path('policy', 'deny', 'check.js'); const { status, stdout } = spawnSync( process.execPath, [ - '--policy-deny', 'fs.in,fs.out', dep + '--policy-deny-fs', 'out,in', '-e', + `console.log(process.policy.check("fs")); + console.log(process.policy.check("fs.in")); + console.log(process.policy.check("fs.out"));` ] ); const [fs, fsIn, fsOut] = stdout.toString().split('\n'); - assert.strictEqual(fs, 'true'); + assert.strictEqual(fs, 'false'); assert.strictEqual(fsIn, 'false'); assert.strictEqual(fsOut, 'false'); assert.strictEqual(status, 0); @@ -74,9 +87,30 @@ const dep = fixtures.path('policy', 'deny', 'check.js'); { const { status, stderr } = spawnSync( process.execPath, - ['--policy-deny=fs.in', '-p', 'fs.readFileSync(process.execPath)']); + ['--policy-deny-fs=in', '-p', 'fs.readFileSync(process.execPath)']); + assert.ok( + stderr.toString().includes('Access to this API has been restricted'), + stderr); + assert.strictEqual(status, 1); +} + +{ + const { status, stderr } = spawnSync( + process.execPath, + ['--policy-deny-fs=fs', '-p', 'fs.readFileSync(process.execPath)']); + assert.ok( + stderr.toString().includes('Access to this API has been restricted'), + stderr); + assert.strictEqual(status, 1); +} + +{ + const { status, stderr } = spawnSync( + process.execPath, + ['--policy-deny-fs=out', '-p', 'fs.writeFileSync("policy-deny-example.md", "# test")']); assert.ok( stderr.toString().includes('Access to this API has been restricted'), stderr); assert.strictEqual(status, 1); + assert.ok(fs.existsSync('policy-deny-example.md')); } diff --git a/test/parallel/test-policy-deny-fs-in.js b/test/parallel/test-policy-deny-fs-in.js new file mode 100644 index 00000000000000..743f539db79181 --- /dev/null +++ b/test/parallel/test-policy-deny-fs-in.js @@ -0,0 +1,281 @@ +// Flags: --policy-deny-fs=in:.gitignore:/tmp/ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fs = require('fs') + +const blockedFile = '.gitignore'; +const blockedFolder = '/tmp/'; +const regularFile = __filename; + +// fs.readFileSync +{ + assert.throws(() => { + fs.readFileSync(blockedFile); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.throws(() => { + fs.readFileSync(blockedFolder + 'anyfile'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.readFileSync(regularFile); + }); +} + +// fs.readFile +{ + assert.throws(() => { + fs.readFile(blockedFile, () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.throws(() => { + fs.readFile(blockedFolder + 'anyfile', () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.readFile(regularFile, () => {}); + }); +} + +// fs.createReadStream +{ + assert.rejects(() => { + return new Promise((_resolve, reject) => { + const stream = fs.createReadStream(blockedFile); + stream.on('error', reject) + }) + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.rejects(() => { + return new Promise((_resolve, reject) => { + const stream = fs.createReadStream(blockedFile); + stream.on('error', reject) + }) + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotReject(() => { + return new Promise((_resolve, reject) => { + const stream = fs.createReadStream(regularFile); + stream.on('error', reject) + }) + }); +} + +// fs.statSync +{ + assert.throws(() => { + fs.statSync(blockedFile); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.throws(() => { + fs.statSync(blockedFolder + 'anyfile'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.statSync(regularFile); + }); +} + +// fs.stat +{ + assert.throws(() => { + fs.stat(blockedFile, () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.throws(() => { + fs.stat(blockedFolder + 'anyfile', () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.stat(regularFile, () => {}); + }); +} + +// fs.accessSync +{ + assert.throws(() => { + fs.accessSync(blockedFile, fs.constants.R_OK); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.throws(() => { + fs.accessSync(blockedFolder + 'anyfile', fs.constants.R_OK); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.accessSync(regularFile, fs.constants.R_OK); + }); +} + +// fs.access +{ + assert.throws(() => { + fs.access(blockedFile, fs.constants.R_OK, () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.throws(() => { + fs.access(blockedFolder + 'anyfile', fs.constants.R_OK, () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.access(regularFile, fs.constants.R_OK, () => {}); + }); +} + +// fs.chmodSync (should not bypass) +{ + assert.throws(() => { + // this operation will work fine + fs.chmodSync(blockedFile, 0o400); + fs.readFileSync(blockedFile) + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); +} + +// fs.chownSync (should not bypass) +{ + assert.throws(() => { + // this operation will work fine + fs.chownSync(blockedFile, process.getuid(), process.getgid()); + fs.readFileSync(blockedFile) + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); +} + +// TODO(rafaelgss): mention possible workarounds (spawn('cp blockedFile regularFile')) +// copyFile (handle security concerns) +// cp (handle security concerns) + +// fs.openSync +{ + assert.throws(() => { + fs.openSync(blockedFile, 'r'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.throws(() => { + fs.openSync(blockedFolder + 'anyfile', 'r'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.openSync(regularFile, 'r'); + }); +} + +// fs.open +{ + assert.throws(() => { + fs.open(blockedFile, 'r', () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.throws(() => { + fs.open(blockedFolder + 'anyfile', 'r', () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.open(regularFile, 'r', () => {}); + }); +} + +// fs.opendir (TODO) +{ + assert.throws(() => { + fs.opendir(blockedFolder, () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.opendir(__dirname, () => {}); + }); +} + +// fs.readdir +{ + assert.throws(() => { + fs.readdir(blockedFolder, () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.readdir(__dirname, () => {}); + }); +} + +// fs.watch (TODO) +{ + assert.throws(() => { + fs.watch(blockedFile, () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.readdir(__dirname, () => {}); + }); +} diff --git a/test/parallel/test-policy-deny-fs-out.js b/test/parallel/test-policy-deny-fs-out.js new file mode 100644 index 00000000000000..72c1a86ee3cf06 --- /dev/null +++ b/test/parallel/test-policy-deny-fs-out.js @@ -0,0 +1,250 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +const blockedFolder = fixtures.path('policy', 'deny', 'protected-folder'); +const blockedFile = fixtures.path('policy', 'deny', 'protected-file.md'); + +const regularFolder = fixtures.path('policy', 'deny'); +const regularFile = fixtures.path('policy', 'deny', 'regular-file.md'); + +{ + assert.ok(process.policy.deny('fs.out', blockedFolder)); + assert.ok(process.policy.deny('fs.out', blockedFile)); +} + +// fs.writeFileSync +{ + assert.throws(() => { + fs.writeFileSync(blockedFile, 'example'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + assert.throws(() => { + fs.writeFileSync(blockedFolder + 'anyfile', 'example'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +} + +// fs.writeFile +{ + assert.throws(() => { + fs.writeFile(blockedFile, 'example', () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + assert.throws(() => { + fs.writeFile(blockedFolder + 'anyfile', 'example', () => {}); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +} + +// fs.writeSync +{ + assert.throws(() => { + fs.writeSync(fs.openSync(blockedFile, 'w'), 'example') + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + assert.throws(() => { + fs.writeSync(fs.openSync(blockedFolder + 'anyfile', 'w'), 'example'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +} + +// fs.createWriteStream +{ + assert.rejects(() => { + return new Promise((_resolve, reject) => { + const stream = fs.createWriteStream(blockedFile); + stream.on('error', reject); + }) + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + assert.rejects(() => { + return new Promise((_resolve, reject) => { + const stream = fs.createReadStream(blockedFolder + 'example'); + stream.on('error', reject); + }) + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +} + +// fs.utimesSync +{ + assert.throws(() => { + fs.utimesSync(blockedFile, new Date(), new Date()); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + assert.throws(() => { + fs.utimesSync(blockedFile + 'anyfile', new Date(), new Date()); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +} + +// fs.copyFileSync +{ + assert.throws(() => { + fs.copyFileSync(blockedFile, blockedFolder + 'any-other-file'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + // TODO(rafaelgss): should it throw when copying (reading) from a + // blockedFile (out) to a regular folder? + assert.throws(() => { + fs.copyFileSync(blockedFile, regularFolder + 'any-other-file'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + assert.throws(() => { + fs.copyFileSync(regularFile, blockedFolder + 'any-file'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +} + +// fs.cpSync +{ + assert.throws(() => { + fs.cpSync(blockedFile, blockedFolder + 'any-other-file'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + // TODO(rafaelgss): should it throw when copying (reading) from a + // blockedFile (out) to a regular folder? + assert.throws(() => { + fs.cpSync(blockedFile, regularFolder + 'any-other-file'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + assert.throws(() => { + fs.cpSync(regularFile, blockedFolder + 'any-file'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +} + +// fs.mkdir +{ + assert.throws(() => { + fs.mkdir(blockedFolder + 'any-folder', (err) => { + if (err) throw err; + }); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +} + +// fs.rename +{ + assert.throws(() => { + fs.rename(blockedFile, blockedFile + 'renamed', (err) => { + if (err) throw err; + }); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + assert.throws(() => { + fs.rename(blockedFile, regularFolder + 'renamed', (err) => { + if (err) throw err; + }); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + assert.throws(() => { + fs.rename(regularFile, blockedFolder + 'renamed', (err) => { + if (err) throw err; + }); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +} + +// fs.rmdir +{ + assert.throws(() => { + fs.rmdir(blockedFolder, (err) => { + if (err) throw err; + }); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + // TODO(rafaelgss): the user should be capable to rmdir of a non-protected + // folder but that contains a protected file? + // regularFolder contains a protected file + assert.throws(() => { + fs.rmdir(regularFolder, (err) => { + if (err) throw err; + }); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +} + +{ + assert.throws(() => { + fs.rm(blockedFolder, (err) => { + if (err) throw err; + }); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); + + // regularFolder contains a protected file + assert.throws(() => { + fs.rm(regularFolder, (err) => { + if (err) throw err; + }); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +} diff --git a/test/parallel/test-policy-deny-fs.js b/test/parallel/test-policy-deny-fs.js deleted file mode 100644 index dfd5bd1a526d35..00000000000000 --- a/test/parallel/test-policy-deny-fs.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); - -const fixtures = require('../common/fixtures'); - -const { spawnSync } = require('child_process'); -const assert = require('assert') - -const dep = fixtures.path('policy', 'deny', 'fs', 'read-file.js'); -// policy.check -{ - const { status, stderr } = spawnSync( - process.execPath, - [ - '--policy-deny', 'fs', dep - ] - ); - - assert.strictEqual(status, 1); - assert( - stderr.toString() - .includes('Error: Access to this API has been restricted') - ); -} - -{ - const { status, stderr } = spawnSync( - process.execPath, - [ - '--policy-deny', 'fs.in', dep - ] - ); - - assert.strictEqual(status, 1); - assert( - stderr.toString() - .includes('Error: Access to this API has been restricted') - ); -} - -{ - const { status, stderr } = spawnSync( - process.execPath, - [ - '--policy-deny', 'fs.out', dep - ] - ); - - assert.strictEqual(status, 0); -} diff --git a/test/parallel/test-policy-deny.js b/test/parallel/test-policy-deny.js new file mode 100644 index 00000000000000..8df2dee1f62d82 --- /dev/null +++ b/test/parallel/test-policy-deny.js @@ -0,0 +1,77 @@ +const common = require('../common'); +const fs = require('fs'); +const fsPromises = require('node:fs/promises'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + + +const protectedFolder = fixtures.path('policy', 'deny'); +const protectedFile = fixtures.path('policy', 'deny', 'protected-file.md'); +const regularFile = fixtures.path('policy', 'deny', 'regular-file.md'); + +// assert check and deny exists +{ + assert.ok(typeof process.policy.check === 'function'); + assert.ok(typeof process.policy.deny === 'function'); +} + +// Guarantee the initial state when no flags +{ + assert.ok(process.policy.check('fs.in')); + assert.ok(process.policy.check('fs.out')); + + assert.ok(process.policy.check('fs.in', protectedFile)); + assert.ok(process.policy.check('fs.in', regularFile)); + + assert.ok(process.policy.check('fs.out', protectedFolder)); + assert.ok(process.policy.check('fs.out', regularFile)); + + assert.doesNotReject(() => { + return fsPromises.readFile(protectedFile); + }); +} + +// Deny access to fs.in +{ + assert.ok(process.policy.deny('fs.in', [protectedFile])); + assert.ok(process.policy.check('fs.in')); + assert.ok(process.policy.check('fs.out')); + + assert.ok(!process.policy.check('fs.in', protectedFile)); + assert.ok(process.policy.check('fs.in', regularFile)); + + assert.ok(process.policy.check('fs.out', protectedFolder)); + assert.ok(process.policy.check('fs.out', regularFile)); + + assert.rejects(() => { + return fsPromises.readFile(protectedFile); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemIn', + })); + + assert.doesNotThrow(() => { + fs.open(regularFile, () => {}); + }); +} + +// Deny access to fs.out +{ + assert.ok(process.policy.deny('fs.out', [protectedFolder])); + assert.ok(process.policy.check('fs.in')); + assert.ok(process.policy.check('fs.out')); + + assert.ok(!process.policy.check('fs.in', protectedFile)); + assert.ok(process.policy.check('fs.in', regularFile)); + + assert.ok(!process.policy.check('fs.out', protectedFolder)); + assert.ok(!process.policy.check('fs.out', regularFile)); + + assert.rejects(() => { + return fsPromises + .writeFile(protectedFolder + '/new-file', 'data'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemOut', + })); +}