From 85373119c0757634d237733f85b5d51ee384b8bb Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 3 Aug 2018 22:37:09 +0200 Subject: [PATCH 1/3] process: initial impl of feature access control Implement `process.accessControl`, a simple API for restricting usage of certain in-process APIs. Refs: https://github.com/nodejs/node/issues/22107 --- doc/api/errors.md | 9 ++ doc/api/process.md | 156 ++++++++++++++++++++++++++ lib/internal/bootstrap/node.js | 2 + node.gyp | 1 + src/cares_wrap.cc | 3 + src/env-inl.h | 25 +++++ src/env.h | 57 ++++++++++ src/fs_event_wrap.cc | 1 + src/inspector_js_api.cc | 7 +- src/module_wrap.cc | 2 + src/node.cc | 15 +++ src/node_access_control.cc | 123 +++++++++++++++++++++ src/node_errors.h | 2 + src/node_file.cc | 46 +++++++- src/node_internals.h | 1 + src/node_os.cc | 2 + src/node_process.cc | 8 ++ src/node_stat_watcher.cc | 3 + src/node_trace_events.cc | 2 + src/node_v8.cc | 3 + src/node_worker.cc | 2 + src/pipe_wrap.cc | 5 + src/process_wrap.cc | 5 + src/spawn_sync.cc | 1 + src/tcp_wrap.cc | 41 +++++++ src/udp_wrap.cc | 4 + test/parallel/test-access-control.js | 159 +++++++++++++++++++++++++++ 27 files changed, 682 insertions(+), 3 deletions(-) create mode 100644 src/node_access_control.cc create mode 100644 test/parallel/test-access-control.js diff --git a/doc/api/errors.md b/doc/api/errors.md index e4a709d45a8031..b60bdb7f11d3ed 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -578,6 +578,14 @@ found [here][online]. ## Node.js Error Codes + +### ERR_ACCESS_DENIED + +> Stability: 1 - Experimental + +This error is thrown when an attempt was made to use a feature of Node.js +which was previously disabled through the [`process.accessControl`][] mechanism. + ### ERR_AMBIGUOUS_ARGUMENT @@ -1867,6 +1875,7 @@ A module file could not be resolved while attempting a [`require()`][] or [`net`]: net.html [`new URL(input)`]: url.html#url_constructor_new_url_input_base [`new URLSearchParams(iterable)`]: url.html#url_constructor_new_urlsearchparams_iterable +[`process.accessControl`]: process.html#process_access_control [`process.send()`]: process.html#process_process_send_message_sendhandle_options_callback [`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn [`require()`]: modules.html#modules_require diff --git a/doc/api/process.md b/doc/api/process.md index 0c35007103c253..7d4f15e2f2ab5a 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -1996,6 +1996,141 @@ Will generate an object similar to: tz: '2016b' } ``` +## Access Control + +> Stability: 1 - Experimental + +### process.accessControl.apply(restrictions) + + +* `restrictions` {Object} A set of access restrictions that will be applied + to the current Node.js instance. The format should be the same as the one + returned by [`process.accessControl.getCurrent()`][]. Omitted keys will + default to `true`, i.e. to retaining the relevant permissions. + +*Warning*: This does not provide a full isolation mechanism. Existing resources +and communication channels may be used to circumvent these measures, if they +have been made available before the corresponding restrictions have been put +into place. + +This API is recent and may not be complete. +Please report bugs at https://github.com/nodejs/node/issues. + +Operations started before this call are not undone or stopped. +Features that were previously disabled through this call cannot be re-enabled. +Child processes do not inherit these restrictions, whereas [`Worker`][]s do. + +The following code removes some permissions of the current Node.js instance: + +```js +process.accessControl.apply({ + childProcesses: false, + createWorkers: false, + fsRead: false, + fsWrite: false, + loadAddons: false, + setV8Flags: false +}); +``` + +Keys not listed in the object, or with values not set to `false`, +are unaffected. + +If a key that is not `childProcesses` or `loadAddons` is set to `false`, +the `childProcesses` and `loadAddons` feature set will also be disabled +automatically. + +See [`process.accessControl.getCurrent()`][] for a list of permissions. + +### process.accessControl.getCurrent() + + +* Returns: {Object} An object representing a set of permissions for the + current Node.js instance of the following structure: + + +```js +{ accessInspectorBindings: true, + childProcesses: true, + createWorkers: true, + fsRead: true, + fsWrite: true, + loadAddons: true, + modifyTraceEvents: true, + netConnectionless: true, + netIncoming: true, + netOutgoing: true, + setEnvironmentVariables: true, + setProcessAttrs: true, + setV8Flags: true, + signalProcesses: true } +``` + +Currently only boolean options are supported. + +In particular, these settings affect the following features of the Node.js API: + +- `accessInspectorBindings`: + - [`inspector.open()`][] + - [`inspector.Session.connect()`][] +- `childProcesses`: + - [`child_process`][`ChildProcess`] methods +- `createWorkers`: + - [`worker_threads.Worker`][`Worker`] +- `fsRead`: + - All read-only [`fs`][] operations, including watchers + - This always includes `fs.open()` and `fs.openSync()`, even when + writing to files. + - Existing `fs.ReadStream` instances will not continue to work. + - [`os.homedir()`][] + - [`os.userInfo()`][] + - [`require()`][] + - [`process.stdin`][] if this stream points to a file. + - Note that `fsRead` and `fsWrite` are distinct permissions. +- `fsWrite`: + - All other [`fs`][] operations + - `fs.open()` and `fs.openSync()` when flags indicate writing or appending + - Existing `fs.WriteStream` instances will not continue to work. + - [`net`][] operations on UNIX domain sockets + - [`process.stdout`][] and [`process.stderr`][], respectively, if those + streams point to files. +- `loadAddons`: + - [`process.dlopen()`][] and [`require()`][] in the case of native addons +- `modifyTraceEvents`: + - [`tracing.disable()`][] and [`tracing.enable()`][] +- `netConnectionless`: + - [UDP][] operations, including sending data from existing sockets +- `netIncoming`: + - [`server.listen()`][`net.Server`] + - Receiving or sending data on existing sockets is unaffected. +- `netOutgoing`: + - [`socket.connect()`][`net.Socket`] + - [`dns`][] requests + - Receiving or sending data on existing sockets is unaffected. +- `setEnvironmentVariables`: + - Setting/deleting keys on [`process.env`][] +- `setProcessAttrs`: + - [`process.chdir()`][] + - [`process.initgroups()`][] + - [`process.setgroups()`][] + - [`process.setgid()`][] + - [`process.setuid()`][] + - [`process.setegid()`][] + - [`process.seteuid()`][] + - [`process.title`][] setting +- `setV8Flags`: + - [`v8.setFlagsFromString()`][] +- `signalProcesses`: + - [`process.kill()`][] + - Debugging cluster child processes + +This function always returns a new object. Modifications to the returned object +will have no effect. + ## Exit Codes Node.js will normally exit with a `0` status code when no more async @@ -2057,24 +2192,44 @@ cases: [`Worker`]: worker_threads.html#worker_threads_class_worker [`console.error()`]: console.html#console_console_error_data_args [`console.log()`]: console.html#console_console_log_data_args +[`dns`]: dns.html [`domain`]: domain.html [`end()`]: stream.html#stream_writable_end_chunk_encoding_callback +[`fs`]: fs.html +[`inspector.open()`]: inspector.html#inspector_inspector_open_port_host_wait +[`inspector.Session.connect()`]: inspector.html#inspector_session_connect +[`net`]: net.html [`net.Server`]: net.html#net_class_net_server [`net.Socket`]: net.html#net_class_net_socket [`os.constants.dlopen`]: os.html#os_dlopen_constants +[`os.homedir()`]: os.html#os_os_homedir +[`os.userInfo()`]: os.html#os_os_userinfo_options +[`process.accessControl.getCurrent()`]: #process_process_accesscontrol_getcurrent [`process.argv`]: #process_process_argv +[`process.chdir()`]: #process_process_chdir_directory +[`process.dlopen()`]: #process_process_dlopen_module_filename_flags +[`process.env`]: #process_process_env [`process.execPath`]: #process_process_execpath [`process.exit()`]: #process_process_exit_code [`process.exitCode`]: #process_process_exitcode [`process.hrtime()`]: #process_process_hrtime_time [`process.hrtime.bigint()`]: #process_process_hrtime_bigint +[`process.initgroups()`]: #process_process_initgroups_user_extragroup [`process.kill()`]: #process_process_kill_pid_signal +[`process.setegid()`]: #process_process_setegid_id +[`process.seteuid()`]: #process_process_seteuid_id +[`process.setgid()`]: #process_process_setgid_id +[`process.setgroups()`]: #process_process_setgroups_groups +[`process.setuid()`]: #process_process_setuid_id [`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn +[`process.title`]: #process_process_title [`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch [`require()`]: globals.html#globals_require [`require.main`]: modules.html#modules_accessing_the_main_module [`require.resolve()`]: modules.html#modules_require_resolve_request_options [`setTimeout(fn, 0)`]: timers.html#timers_settimeout_callback_delay_args +[`tracing.disable()`]: tracing.html#tracing_tracing_disable +[`tracing.enable()`]: tracing.html#tracing_tracing_enable [`v8.setFlagsFromString()`]: v8.html#v8_v8_setflagsfromstring_flags [Android building]: https://github.com/nodejs/node/blob/master/BUILDING.md#androidandroid-based-devices-eg-firefox-os [Child Process]: child_process.html @@ -2089,4 +2244,5 @@ cases: [Signal Events]: #process_signal_events [Stream compatibility]: stream.html#stream_compatibility_with_older_node_js_versions [TTY]: tty.html#tty_tty +[UDP]: dgram.html [Writable]: stream.html#stream_writable_streams diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index e6c4da25b00d9b..9d0dc5409ebb30 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -98,6 +98,8 @@ perThreadSetup.setupMemoryUsage(_memoryUsage); perThreadSetup.setupKillAndExit(); + process.accessControl = internalBinding('access_control'); + if (global.__coverage__) NativeModule.require('internal/process/write-coverage').setup(); diff --git a/node.gyp b/node.gyp index 8c42474bbed90a..d6d9530f00f16f 100644 --- a/node.gyp +++ b/node.gyp @@ -337,6 +337,7 @@ 'src/js_stream.cc', 'src/module_wrap.cc', 'src/node.cc', + 'src/node_access_control.cc', 'src/node_api.cc', 'src/node_api.h', 'src/node_api_types.h', diff --git a/src/cares_wrap.cc b/src/cares_wrap.cc index 9d3d098734470f..849148add59ca0 100644 --- a/src/cares_wrap.cc +++ b/src/cares_wrap.cc @@ -1800,6 +1800,7 @@ class GetHostByAddrWrap: public QueryWrap { template static void Query(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, netOutgoing); ChannelWrap* channel; ASSIGN_OR_RETURN_UNWRAP(&channel, args.Holder()); @@ -1949,6 +1950,7 @@ void CanonicalizeIP(const FunctionCallbackInfo& args) { void GetAddrInfo(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, netOutgoing); CHECK(args[0]->IsObject()); CHECK(args[1]->IsString()); @@ -2000,6 +2002,7 @@ void GetAddrInfo(const FunctionCallbackInfo& args) { void GetNameInfo(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, netOutgoing); CHECK(args[0]->IsObject()); CHECK(args[1]->IsString()); diff --git a/src/env-inl.h b/src/env-inl.h index 3bca20c81ce4ce..fa61d664588fb3 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -55,6 +55,27 @@ inline MultiIsolatePlatform* IsolateData::platform() const { return platform_; } +AccessControl::AccessControl() { + permissions_.set(); // Sets all values to 'true'. +} + +void AccessControl::set_permission(Permission perm, bool value) { + permissions_.set(static_cast(perm), value); + + if (value == false && perm != childProcesses && perm != loadAddons) { + set_permission(childProcesses, false); + set_permission(loadAddons, false); + } +} + +bool AccessControl::has_permission(Permission perm) const { + return LIKELY(permissions_.test(static_cast(perm))); +} + +void AccessControl::apply(const AccessControl& other) { + permissions_ &= other.permissions_; +} + inline Environment::AsyncHooks::AsyncHooks() : async_ids_stack_(env()->isolate(), 16 * 2), fields_(env()->isolate(), kFieldsCount), @@ -391,6 +412,10 @@ inline uv_loop_t* Environment::event_loop() const { return isolate_data()->event_loop(); } +inline AccessControl* Environment::access_control() { + return &access_control_; +} + inline Environment::AsyncHooks* Environment::async_hooks() { return &async_hooks_; } diff --git a/src/env.h b/src/env.h index 28aa936c246d25..f9112475ead094 100644 --- a/src/env.h +++ b/src/env.h @@ -36,6 +36,7 @@ #include "node.h" #include "node_http2_state.h" +#include #include #include #include @@ -235,6 +236,7 @@ struct PackageConfig { V(password_string, "password") \ V(path_string, "path") \ V(pending_handle_string, "pendingHandle") \ + V(permission_string, "permission") \ V(pid_string, "pid") \ V(pipe_string, "pipe") \ V(pipe_target_string, "pipeTarget") \ @@ -418,6 +420,59 @@ enum class DebugCategory { CATEGORY_COUNT }; +class AccessControl { + public: + enum Permission { +#define ACCESS_CONTROL_FLAGS(V) \ + V(accessInspectorBindings) \ + V(childProcesses) \ + V(createWorkers) \ + V(fsRead) \ + V(fsWrite) \ + V(loadAddons) \ + V(modifyTraceEvents) \ + V(netConnectionless) \ + V(netIncoming) \ + V(netOutgoing) \ + V(setEnvironmentVariables) \ + V(setProcessAttrs) \ + V(setV8Flags) \ + V(signalProcesses) \ + +#define V(kind) kind, + ACCESS_CONTROL_FLAGS(V) +#undef V + kNumPermissions + }; + + inline AccessControl(); + + inline void set_permission(Permission perm, bool value); + inline bool has_permission(Permission perm) const; + inline void apply(const AccessControl& other); + + static Permission PermissionFromString(const char* str); + static const char* PermissionToString(Permission perm); + + v8::MaybeLocal ToObject(v8::Local context); + static v8::Maybe FromObject(v8::Local context, + v8::Local object); + + static void ThrowAccessDenied(Environment* env, Permission perm); + + private: + std::bitset permissions_; +}; + +#define THROW_IF_INSUFFICIENT_PERMISSIONS(env, perm_, ...) \ + do { \ + const AccessControl::Permission perm = (AccessControl::Permission::perm_); \ + if (UNLIKELY(!(env)->access_control()->has_permission(perm))) { \ + AccessControl::ThrowAccessDenied((env), perm); \ + return __VA_ARGS__; \ + } \ + } while (0) + class Environment { public: class AsyncHooks { @@ -627,6 +682,7 @@ class Environment { inline void IncreaseWaitingRequestCounter(); inline void DecreaseWaitingRequestCounter(); + inline AccessControl* access_control(); inline AsyncHooks* async_hooks(); inline ImmediateInfo* immediate_info(); inline TickInfo* tick_info(); @@ -897,6 +953,7 @@ class Environment { uv_check_t idle_check_handle_; bool profiler_idle_notifier_started_ = false; + AccessControl access_control_; AsyncHooks async_hooks_; ImmediateInfo immediate_info_; TickInfo tick_info_; diff --git a/src/fs_event_wrap.cc b/src/fs_event_wrap.cc index e0cdab4d3e20f1..349b1026e0ffa5 100644 --- a/src/fs_event_wrap.cc +++ b/src/fs_event_wrap.cc @@ -134,6 +134,7 @@ 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, fsRead); FSEventWrap* wrap = Unwrap(args.This()); CHECK_NOT_NULL(wrap); diff --git a/src/inspector_js_api.cc b/src/inspector_js_api.cc index 4e95598d3a0580..ce2abb07d0e55d 100644 --- a/src/inspector_js_api.cc +++ b/src/inspector_js_api.cc @@ -75,6 +75,7 @@ class JSBindingsConnection : public AsyncWrap { static void New(const FunctionCallbackInfo& info) { Environment* env = Environment::GetCurrent(info); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, accessInspectorBindings); CHECK(info[0]->IsFunction()); Local callback = info[0].As(); new JSBindingsConnection(env, info.This(), callback); @@ -122,7 +123,8 @@ static bool InspectorEnabled(Environment* env) { } void AddCommandLineAPI(const FunctionCallbackInfo& info) { - auto env = Environment::GetCurrent(info); + Environment* env = Environment::GetCurrent(info); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, accessInspectorBindings); Local context = env->context(); // inspector.addCommandLineAPI takes 2 arguments: a string and a value. @@ -135,6 +137,7 @@ void AddCommandLineAPI(const FunctionCallbackInfo& info) { void CallAndPauseOnStart(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, accessInspectorBindings); CHECK_GT(args.Length(), 1); CHECK(args[0]->IsFunction()); SlicedArguments call_args(args, /* start */ 2); @@ -237,6 +240,8 @@ void IsEnabled(const FunctionCallbackInfo& args) { void Open(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, accessInspectorBindings); + Agent* agent = env->inspector_agent(); bool wait_for_connect = false; diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 63e886ab0d7ac0..579cee792d0521 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -640,6 +640,7 @@ Maybe Resolve(Environment* env, const std::string& specifier, const URL& base, PackageMainCheck check_pjson_main) { + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead, Nothing()); URL pure_url(specifier); if (!(pure_url.flags() & URL_FLAGS_FAILED)) { // just check existence, without altering @@ -668,6 +669,7 @@ Maybe Resolve(Environment* env, void ModuleWrap::Resolve(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); // module.resolve(specifier, url) CHECK_EQ(args.Length(), 2); diff --git a/src/node.cc b/src/node.cc index 33a68b37a46062..3982ac972f7391 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1380,6 +1380,7 @@ void InitModpendingOnce() { // cache that's a plain C list or hash table that's shared across contexts? static void DLOpen(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, loadAddons); auto context = env->context(); uv_once(&init_modpending_once, InitModpendingOnce); @@ -1799,6 +1800,9 @@ static void ProcessTitleGetter(Local property, static void ProcessTitleSetter(Local property, Local value, const PropertyCallbackInfo& info) { + Environment* env = Environment::GetCurrent(info); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setProcessAttrs); + node::Utf8Value title(info.GetIsolate(), value); TRACE_EVENT_METADATA1("__metadata", "process_name", "name", TRACE_STR_COPY(*title)); @@ -1844,6 +1848,8 @@ static void EnvSetter(Local property, Local value, const PropertyCallbackInfo& info) { Environment* env = Environment::GetCurrent(info); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setEnvironmentVariables); + if (config_pending_deprecation && env->EmitProcessEnvWarning() && !value->IsString() && !value->IsNumber() && !value->IsBoolean()) { if (ProcessEmitDeprecationWarning( @@ -1906,6 +1912,9 @@ static void EnvQuery(Local property, static void EnvDeleter(Local property, const PropertyCallbackInfo& info) { + Environment* env = Environment::GetCurrent(info); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setEnvironmentVariables); + Mutex::ScopedLock lock(environ_mutex); if (property->IsString()) { #ifdef __POSIX__ @@ -2050,6 +2059,9 @@ static void DebugPortGetter(Local property, static void DebugPortSetter(Local property, Local value, const PropertyCallbackInfo& info) { + Environment* env = Environment::GetCurrent(info); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setProcessAttrs); + Mutex::ScopedLock lock(process_mutex); debug_options.set_port(value->Int32Value()); } @@ -3122,6 +3134,7 @@ void RegisterSignalHandler(int signal, void DebugProcess(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, signalProcesses); if (args.Length() != 1) { return env->ThrowError("Invalid number of arguments."); @@ -3148,6 +3161,8 @@ static int GetDebugSignalHandlerMappingName(DWORD pid, wchar_t* buf, static void DebugProcess(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, signalProcesses); + Isolate* isolate = args.GetIsolate(); DWORD pid; HANDLE process = nullptr; diff --git a/src/node_access_control.cc b/src/node_access_control.cc new file mode 100644 index 00000000000000..efa2eb53d65423 --- /dev/null +++ b/src/node_access_control.cc @@ -0,0 +1,123 @@ +#include "node_internals.h" +#include "node_errors.h" +#include "env-inl.h" + +using v8::Boolean; +using v8::Context; +using v8::EscapableHandleScope; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Isolate; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::Value; + +namespace node { + +MaybeLocal AccessControl::ToObject(Local context) { + Isolate* isolate = context->GetIsolate(); + EscapableHandleScope handle_scope(isolate); + + Local obj = Object::New(isolate); + +#define V(kind) \ + if (obj->Set(context, \ + FIXED_ONE_BYTE_STRING(isolate, #kind), \ + Boolean::New(isolate, has_permission(kind))).IsNothing()) { \ + return MaybeLocal(); \ + } + ACCESS_CONTROL_FLAGS(V) +#undef V + + return handle_scope.Escape(obj); +} + +Maybe AccessControl::FromObject(Local context, + Local obj) { + Isolate* isolate = context->GetIsolate(); + HandleScope scope(isolate); + + AccessControl ret; + + Local field; +#define V(kind) \ + if (!obj->Get(context, FIXED_ONE_BYTE_STRING(isolate, #kind)) \ + .ToLocal(&field)) { \ + return Nothing(); \ + } \ + ret.set_permission(kind, !field->IsFalse()); + ACCESS_CONTROL_FLAGS(V) +#undef V + + return Just(ret); +} + +AccessControl::Permission AccessControl::PermissionFromString(const char* str) { +#define V(kind) if (strcmp(str, #kind) == 0) return kind; + ACCESS_CONTROL_FLAGS(V) +#undef V + return kNumPermissions; +} + +const char* AccessControl::PermissionToString(Permission perm) { + switch (perm) { +#define V(kind) case kind: return #kind; + ACCESS_CONTROL_FLAGS(V) +#undef V + default: return nullptr; + } +} + +void AccessControl::ThrowAccessDenied(Environment* env, Permission perm) { + Local err = ERR_ACCESS_DENIED(env->isolate()); + CHECK(err->IsObject()); + err.As()->Set( + env->context(), + env->permission_string(), + v8::String::NewFromUtf8(env->isolate(), + PermissionToString(perm), + v8::NewStringType::kNormal).ToLocalChecked()) + .FromMaybe(false); // Nothing to do about an error at this point. + env->isolate()->ThrowException(err); +} + +namespace ac { + +void Apply(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + AccessControl access_control; + Local obj; + if (!args[0]->ToObject(env->context()).ToLocal(&obj) || + !AccessControl::FromObject(env->context(), obj).To(&access_control)) { + return; + } + + env->access_control()->apply(access_control); +} + +void GetCurrent(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + Local ret; + if (env->access_control()->ToObject(env->context()).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void Initialize(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + env->SetMethod(target, "apply", Apply); + env->SetMethod(target, "getCurrent", GetCurrent); +} + +} // namespace ac +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(access_control, node::ac::Initialize) diff --git a/src/node_errors.h b/src/node_errors.h index fdfb670af636af..047f67e76118ad 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -21,6 +21,7 @@ namespace node { // a `Local` containing the TypeError with proper code and message #define ERRORS_WITH_CODE(V) \ + V(ERR_ACCESS_DENIED, Error) \ V(ERR_BUFFER_OUT_OF_BOUNDS, RangeError) \ V(ERR_BUFFER_TOO_LARGE, Error) \ V(ERR_CANNOT_TRANSFER_OBJECT, TypeError) \ @@ -61,6 +62,7 @@ namespace node { // Errors with predefined static messages #define PREDEFINED_ERROR_MESSAGES(V) \ + V(ERR_ACCESS_DENIED, "Access to this API has been restricted") \ V(ERR_CANNOT_TRANSFER_OBJECT, "Cannot transfer object of unsupported type")\ V(ERR_CLOSED_MESSAGE_PORT, "Cannot send data on closed MessagePort") \ V(ERR_CONSTRUCT_CALL_REQUIRED, "Cannot call constructor without `new`") \ diff --git a/src/node_file.cc b/src/node_file.cc index fb976a31a5b3c1..05b8bca022ab92 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -285,6 +285,8 @@ FileHandleReadWrap::FileHandleReadWrap(FileHandle* handle, Local obj) int FileHandle::ReadStart() { if (!IsAlive() || IsClosing()) return UV_EOF; + if (!env()->access_control()->has_permission(AccessControl::fsRead)) + return UV_EPERM; reading_ = true; @@ -783,7 +785,7 @@ inline FSReqBase* GetReqWrap(Environment* env, Local value, void Access(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args.GetIsolate()); - HandleScope scope(env->isolate()); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); const int argc = args.Length(); CHECK_GE(argc, 2); @@ -836,6 +838,8 @@ void Close(const FunctionCallbackInfo& args) { // in the file. static void InternalModuleReadJSON(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); + uv_loop_t* loop = env->event_loop(); CHECK(args[0]->IsString()); @@ -903,6 +907,7 @@ 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, fsRead); CHECK(args[0]->IsString()); node::Utf8Value path(env->isolate(), args[0]); @@ -920,6 +925,7 @@ static void InternalModuleStat(const FunctionCallbackInfo& args) { static void Stat(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); const int argc = args.Length(); CHECK_GE(argc, 2); @@ -950,6 +956,7 @@ static void Stat(const FunctionCallbackInfo& args) { static void LStat(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); const int argc = args.Length(); CHECK_GE(argc, 3); @@ -981,6 +988,7 @@ static void LStat(const FunctionCallbackInfo& args) { static void FStat(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); const int argc = args.Length(); CHECK_GE(argc, 2); @@ -1011,6 +1019,7 @@ static void FStat(const FunctionCallbackInfo& args) { static void Symlink(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); int argc = args.Length(); CHECK_GE(argc, 4); @@ -1039,6 +1048,7 @@ static void Symlink(const FunctionCallbackInfo& args) { static void Link(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); int argc = args.Length(); CHECK_GE(argc, 3); @@ -1065,6 +1075,7 @@ static void Link(const FunctionCallbackInfo& args) { static void ReadLink(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); int argc = args.Length(); CHECK_GE(argc, 3); @@ -1107,6 +1118,7 @@ static void ReadLink(const FunctionCallbackInfo& args) { static void Rename(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); int argc = args.Length(); CHECK_GE(argc, 3); @@ -1133,6 +1145,7 @@ static void Rename(const FunctionCallbackInfo& args) { static void FTruncate(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 3); @@ -1159,6 +1172,7 @@ static void FTruncate(const FunctionCallbackInfo& args) { static void Fdatasync(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 2); @@ -1181,6 +1195,7 @@ static void Fdatasync(const FunctionCallbackInfo& args) { static void Fsync(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 2); @@ -1203,6 +1218,7 @@ static void Fsync(const FunctionCallbackInfo& args) { static void Unlink(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 2); @@ -1225,6 +1241,7 @@ static void Unlink(const FunctionCallbackInfo& args) { static void RMDir(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 2); @@ -1374,6 +1391,7 @@ int MKDirpAsync(uv_loop_t* loop, static void MKDir(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 4); @@ -1408,6 +1426,7 @@ static void MKDir(const FunctionCallbackInfo& args) { static void RealPath(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); const int argc = args.Length(); CHECK_GE(argc, 3); @@ -1451,6 +1470,7 @@ static void RealPath(const FunctionCallbackInfo& args) { static void ReadDir(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); const int argc = args.Length(); CHECK_GE(argc, 3); @@ -1586,6 +1606,11 @@ static void Open(const FunctionCallbackInfo& args) { CHECK(args[1]->IsInt32()); const int flags = args[1].As()->Value(); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); + if ((flags & (O_WRONLY | O_RDWR | O_APPEND | O_CREAT)) != 0) { + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); + } + CHECK(args[2]->IsInt32()); const int mode = args[2].As()->Value(); @@ -1616,6 +1641,11 @@ static void OpenFileHandle(const FunctionCallbackInfo& args) { CHECK(args[1]->IsInt32()); const int flags = args[1].As()->Value(); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); + if ((flags & (O_WRONLY | O_RDWR | O_APPEND | O_CREAT)) != 0) { + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); + } + CHECK(args[2]->IsInt32()); const int mode = args[2].As()->Value(); @@ -1641,6 +1671,7 @@ static void OpenFileHandle(const FunctionCallbackInfo& args) { static void CopyFile(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 3); @@ -1681,7 +1712,7 @@ static void CopyFile(const FunctionCallbackInfo& args) { // if null, write from the current position static void WriteBuffer(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 4); @@ -1733,6 +1764,7 @@ static void WriteBuffer(const FunctionCallbackInfo& args) { // if null, write from the current position static void WriteBuffers(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 3); @@ -1779,6 +1811,7 @@ static void WriteBuffers(const FunctionCallbackInfo& args) { // 3 enc encoding of string static void WriteString(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 4); @@ -1879,6 +1912,7 @@ static void WriteString(const FunctionCallbackInfo& args) { */ static void Read(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); const int argc = args.Length(); CHECK_GE(argc, 5); @@ -1926,6 +1960,7 @@ static void Read(const FunctionCallbackInfo& args) { */ static void Chmod(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 2); @@ -1956,6 +1991,7 @@ static void Chmod(const FunctionCallbackInfo& args) { */ static void FChmod(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 2); @@ -1986,6 +2022,7 @@ static void FChmod(const FunctionCallbackInfo& args) { */ static void Chown(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 3); @@ -2019,6 +2056,7 @@ static void Chown(const FunctionCallbackInfo& args) { */ static void FChown(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 3); @@ -2049,6 +2087,7 @@ static void FChown(const FunctionCallbackInfo& args) { static void LChown(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 3); @@ -2079,6 +2118,7 @@ static void LChown(const FunctionCallbackInfo& args) { static void UTimes(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 3); @@ -2108,6 +2148,7 @@ static void UTimes(const FunctionCallbackInfo& args) { static void FUTimes(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 3); @@ -2137,6 +2178,7 @@ static void FUTimes(const FunctionCallbackInfo& args) { static void Mkdtemp(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite); const int argc = args.Length(); CHECK_GE(argc, 2); diff --git a/src/node_internals.h b/src/node_internals.h index 968d229f1001a7..659cdccc0326d9 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -105,6 +105,7 @@ struct sockaddr; // node is built as static library. No need to depends on the // __attribute__((constructor)) like mechanism in GCC. #define NODE_BUILTIN_STANDARD_MODULES(V) \ + V(access_control) \ V(async_wrap) \ V(buffer) \ V(cares_wrap) \ diff --git a/src/node_os.cc b/src/node_os.cc index c9eef808b0addf..bdbf65f9522e51 100644 --- a/src/node_os.cc +++ b/src/node_os.cc @@ -321,6 +321,7 @@ static void GetInterfaceAddresses(const FunctionCallbackInfo& args) { static void GetHomeDirectory(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); char buf[PATH_MAX]; size_t len = sizeof(buf); @@ -342,6 +343,7 @@ static void GetHomeDirectory(const FunctionCallbackInfo& args) { static void GetUserInfo(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); uv_passwd_t pwd; enum encoding encoding; diff --git a/src/node_process.cc b/src/node_process.cc index d816c32f8dd298..9a12bf9427a26f 100644 --- a/src/node_process.cc +++ b/src/node_process.cc @@ -60,6 +60,7 @@ void Abort(const FunctionCallbackInfo& args) { void Chdir(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setProcessAttrs); CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); @@ -150,6 +151,7 @@ void HrtimeBigInt(const FunctionCallbackInfo& args) { void Kill(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, signalProcesses); if (args.Length() != 2) return env->ThrowError("Bad argument."); @@ -364,6 +366,7 @@ void GetEGid(const FunctionCallbackInfo& args) { void SetGid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK(env->is_main_thread()); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setProcessAttrs); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); @@ -384,6 +387,7 @@ void SetGid(const FunctionCallbackInfo& args) { void SetEGid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK(env->is_main_thread()); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setProcessAttrs); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); @@ -403,6 +407,7 @@ void SetEGid(const FunctionCallbackInfo& args) { void SetUid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setProcessAttrs); CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); @@ -423,6 +428,7 @@ void SetUid(const FunctionCallbackInfo& args) { void SetEUid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setProcessAttrs); CHECK(env->is_main_thread()); CHECK_EQ(args.Length(), 1); @@ -479,6 +485,7 @@ void GetGroups(const FunctionCallbackInfo& args) { void SetGroups(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setProcessAttrs); CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsArray()); @@ -512,6 +519,7 @@ void SetGroups(const FunctionCallbackInfo& args) { void InitGroups(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setProcessAttrs); CHECK_EQ(args.Length(), 2); CHECK(args[0]->IsUint32() || args[0]->IsString()); diff --git a/src/node_stat_watcher.cc b/src/node_stat_watcher.cc index 5e47476fd97a0c..faf7a334c83310 100644 --- a/src/node_stat_watcher.cc +++ b/src/node_stat_watcher.cc @@ -97,11 +97,14 @@ void StatWatcher::Callback(uv_fs_poll_t* handle, void StatWatcher::New(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); new StatWatcher(env, args.This(), args[0]->IsTrue()); } // wrap.start(filename, interval) void StatWatcher::Start(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead); CHECK_EQ(args.Length(), 2); StatWatcher* wrap; diff --git a/src/node_trace_events.cc b/src/node_trace_events.cc index 6e70cfce7bed2e..c19cb0b2b3e9ec 100644 --- a/src/node_trace_events.cc +++ b/src/node_trace_events.cc @@ -63,6 +63,7 @@ void NodeCategorySet::New(const FunctionCallbackInfo& args) { void NodeCategorySet::Enable(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, modifyTraceEvents); NodeCategorySet* category_set; ASSIGN_OR_RETURN_UNWRAP(&category_set, args.Holder()); CHECK_NOT_NULL(category_set); @@ -75,6 +76,7 @@ void NodeCategorySet::Enable(const FunctionCallbackInfo& args) { void NodeCategorySet::Disable(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, modifyTraceEvents); NodeCategorySet* category_set; ASSIGN_OR_RETURN_UNWRAP(&category_set, args.Holder()); CHECK_NOT_NULL(category_set); diff --git a/src/node_v8.cc b/src/node_v8.cc index fb0a9fea1e5d27..52f2a4cdf736de 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -111,6 +111,9 @@ void UpdateHeapSpaceStatisticsBuffer(const FunctionCallbackInfo& args) { void SetFlagsFromString(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, setV8Flags); + CHECK(args[0]->IsString()); String::Utf8Value flags(args.GetIsolate(), args[0]); V8::SetFlagsFromString(*flags, flags.length()); diff --git a/src/node_worker.cc b/src/node_worker.cc index 06edb96804119b..304085eb344f11 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -103,6 +103,7 @@ Worker::Worker(Environment* env, Local wrap) env_->set_abort_on_uncaught_exception(false); env_->set_worker_context(this); env_->set_thread_id(thread_id_); + env_->access_control()->apply(*env->access_control()); env_->Start(0, nullptr, 0, nullptr, env->profiler_idle_notifier_started()); } @@ -340,6 +341,7 @@ Worker::~Worker() { void Worker::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, createWorkers); CHECK(args.IsConstructCall()); diff --git a/src/pipe_wrap.cc b/src/pipe_wrap.cc index a6044b6b267e47..e29baae3265b8f 100644 --- a/src/pipe_wrap.cc +++ b/src/pipe_wrap.cc @@ -163,6 +163,7 @@ PipeWrap::PipeWrap(Environment* env, void PipeWrap::Bind(const FunctionCallbackInfo& args) { PipeWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + THROW_IF_INSUFFICIENT_PERMISSIONS(wrap->env(), fsWrite); node::Utf8Value name(args.GetIsolate(), args[0]); int err = uv_pipe_bind(&wrap->handle_, *name); args.GetReturnValue().Set(err); @@ -182,6 +183,7 @@ void PipeWrap::SetPendingInstances(const FunctionCallbackInfo& args) { void PipeWrap::Fchmod(const v8::FunctionCallbackInfo& args) { PipeWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + THROW_IF_INSUFFICIENT_PERMISSIONS(wrap->env(), fsWrite); CHECK(args[0]->IsInt32()); int mode = args[0].As()->Value(); int err = uv_pipe_chmod(reinterpret_cast(&wrap->handle_), @@ -193,6 +195,7 @@ void PipeWrap::Fchmod(const v8::FunctionCallbackInfo& args) { void PipeWrap::Listen(const FunctionCallbackInfo& args) { PipeWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + THROW_IF_INSUFFICIENT_PERMISSIONS(wrap->env(), fsWrite); int backlog = args[0]->Int32Value(); int err = uv_listen(reinterpret_cast(&wrap->handle_), backlog, @@ -206,6 +209,7 @@ void PipeWrap::Open(const FunctionCallbackInfo& args) { PipeWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + THROW_IF_INSUFFICIENT_PERMISSIONS(wrap->env(), fsWrite); int fd = args[0]->Int32Value(); @@ -222,6 +226,7 @@ void PipeWrap::Connect(const FunctionCallbackInfo& args) { PipeWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + THROW_IF_INSUFFICIENT_PERMISSIONS(wrap->env(), fsWrite); CHECK(args[0]->IsObject()); CHECK(args[1]->IsString()); diff --git a/src/process_wrap.cc b/src/process_wrap.cc index b54e17f21192d8..a7e01a01e84cbb 100644 --- a/src/process_wrap.cc +++ b/src/process_wrap.cc @@ -21,6 +21,7 @@ #include "env-inl.h" #include "handle_wrap.h" +#include "node_errors.h" #include "node_internals.h" #include "node_wrap.h" #include "stream_base-inl.h" @@ -141,6 +142,8 @@ class ProcessWrap : public HandleWrap { static void Spawn(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, childProcesses); + Local context = env->context(); ProcessWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); @@ -282,6 +285,8 @@ class ProcessWrap : public HandleWrap { static void Kill(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, signalProcesses); + ProcessWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); int signal = args[0]->Int32Value(env->context()).FromJust(); diff --git a/src/spawn_sync.cc b/src/spawn_sync.cc index 1e966e42c68ce6..6bc60709d77dac 100644 --- a/src/spawn_sync.cc +++ b/src/spawn_sync.cc @@ -370,6 +370,7 @@ void SyncProcessRunner::Initialize(Local target, void SyncProcessRunner::Spawn(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); + THROW_IF_INSUFFICIENT_PERMISSIONS(env, childProcesses); env->PrintSyncTrace(); SyncProcessRunner p(env); Local result = p.Run(args[0]); diff --git a/src/tcp_wrap.cc b/src/tcp_wrap.cc index e817087d97110b..39eaa19929daf4 100644 --- a/src/tcp_wrap.cc +++ b/src/tcp_wrap.cc @@ -204,6 +204,14 @@ void TCPWrap::Open(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder(), args.GetReturnValue().Set(UV_EBADF)); + + AccessControl* ac = wrap->env()->access_control(); + if (!ac->has_permission(AccessControl::netIncoming) || + !ac->has_permission(AccessControl::netOutgoing)) { + args.GetReturnValue().Set(UV_EPERM); + return; + } + int fd = static_cast(args[0]->IntegerValue()); int err = uv_tcp_open(&wrap->handle_, fd); @@ -219,6 +227,13 @@ void TCPWrap::Bind(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder(), args.GetReturnValue().Set(UV_EBADF)); + + AccessControl* ac = wrap->env()->access_control(); + if (!ac->has_permission(AccessControl::netIncoming)) { + args.GetReturnValue().Set(UV_EPERM); + return; + } + node::Utf8Value ip_address(args.GetIsolate(), args[0]); int port = args[1]->Int32Value(); sockaddr_in addr; @@ -237,6 +252,13 @@ void TCPWrap::Bind6(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder(), args.GetReturnValue().Set(UV_EBADF)); + + AccessControl* ac = wrap->env()->access_control(); + if (!ac->has_permission(AccessControl::netIncoming)) { + args.GetReturnValue().Set(UV_EPERM); + return; + } + node::Utf8Value ip6_address(args.GetIsolate(), args[0]); int port = args[1]->Int32Value(); sockaddr_in6 addr; @@ -255,6 +277,13 @@ void TCPWrap::Listen(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder(), args.GetReturnValue().Set(UV_EBADF)); + + AccessControl* ac = wrap->env()->access_control(); + if (!ac->has_permission(AccessControl::netIncoming)) { + args.GetReturnValue().Set(UV_EPERM); + return; + } + int backlog = args[0]->Int32Value(); int err = uv_listen(reinterpret_cast(&wrap->handle_), backlog, @@ -271,6 +300,12 @@ void TCPWrap::Connect(const FunctionCallbackInfo& args) { args.Holder(), args.GetReturnValue().Set(UV_EBADF)); + AccessControl* ac = wrap->env()->access_control(); + if (!ac->has_permission(AccessControl::netOutgoing)) { + args.GetReturnValue().Set(UV_EPERM); + return; + } + CHECK(args[0]->IsObject()); CHECK(args[1]->IsString()); CHECK(args[2]->IsUint32()); @@ -306,6 +341,12 @@ void TCPWrap::Connect6(const FunctionCallbackInfo& args) { args.Holder(), args.GetReturnValue().Set(UV_EBADF)); + AccessControl* ac = wrap->env()->access_control(); + if (!ac->has_permission(AccessControl::netOutgoing)) { + args.GetReturnValue().Set(UV_EPERM); + return; + } + CHECK(args[0]->IsObject()); CHECK(args[1]->IsString()); CHECK(args[2]->IsUint32()); diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc index 9bcf6ceb7b3ed2..9421a50bb1dc00 100644 --- a/src/udp_wrap.cc +++ b/src/udp_wrap.cc @@ -175,6 +175,7 @@ void UDPWrap::DoBind(const FunctionCallbackInfo& args, int family) { ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder(), args.GetReturnValue().Set(UV_EBADF)); + THROW_IF_INSUFFICIENT_PERMISSIONS(wrap->env(), netConnectionless); // bind(ip, port, flags) CHECK_EQ(args.Length(), 3); @@ -340,6 +341,7 @@ void UDPWrap::DoSend(const FunctionCallbackInfo& args, int family) { ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder(), args.GetReturnValue().Set(UV_EBADF)); + THROW_IF_INSUFFICIENT_PERMISSIONS(wrap->env(), netConnectionless); // send(req, list, list.length, port, address, hasCallback) CHECK(args[0]->IsObject()); @@ -425,6 +427,8 @@ void UDPWrap::RecvStart(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder(), args.GetReturnValue().Set(UV_EBADF)); + THROW_IF_INSUFFICIENT_PERMISSIONS(wrap->env(), netConnectionless); + int err = uv_udp_recv_start(&wrap->handle_, OnAlloc, OnRecv); // UV_EALREADY means that the socket is already bound but that's okay if (err == UV_EALREADY) diff --git a/test/parallel/test-access-control.js b/test/parallel/test-access-control.js new file mode 100644 index 00000000000000..a36a04ad2d9b18 --- /dev/null +++ b/test/parallel/test-access-control.js @@ -0,0 +1,159 @@ +'use strict'; +const common = require('../common'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); +const net = require('net'); + +tmpdir.refresh(); + +assert.deepStrictEqual(process.accessControl.getCurrent(), { + accessInspectorBindings: true, + childProcesses: true, + createWorkers: true, + fsRead: true, + fsWrite: true, + loadAddons: true, + modifyTraceEvents: true, + netConnectionless: true, + netIncoming: true, + netOutgoing: true, + setEnvironmentVariables: true, + setProcessAttrs: true, + setV8Flags: true, + signalProcesses: true +}); + +{ + process.accessControl.apply({ setEnvironmentVariables: false }); + + assert.strictEqual(process.env.CANT_SET_ME, undefined); + + common.expectsError(() => { process.env.CANT_SET_ME = 'bar'; }, { + type: Error, + message: 'Access to this API has been restricted', + code: 'ERR_ACCESS_DENIED' + }); + + assert.strictEqual(process.env.CANT_SET_ME, undefined); + + assert.strictEqual(process.accessControl.getCurrent().setEnvironmentVariables, + false); + + // Setting any other permission to 'false' will also disable child processes + // or loading addons. + assert.strictEqual(process.accessControl.getCurrent().childProcesses, + false); + assert.strictEqual(process.accessControl.getCurrent().loadAddons, + false); + + // Setting values previously set to 'false' back to 'true' should not work. + process.accessControl.apply({ setEnvironmentVariables: true }); + assert.strictEqual(process.accessControl.getCurrent().setEnvironmentVariables, + false); + + process.accessControl.apply({ childProcesses: true }); + assert.strictEqual(process.accessControl.getCurrent().childProcesses, + false); +} + +if (common.isMainThread) { + process.accessControl.apply({ setProcessAttrs: false }); + + common.expectsError(() => { process.title = 'doesntwork'; }, { + type: Error, + message: 'Access to this API has been restricted', + code: 'ERR_ACCESS_DENIED' + }); +} + +{ + // Spin up echo server. + const server = net.createServer((sock) => sock.pipe(sock)) + .listen(0, common.mustCall(() => { + const existingSocket = net.connect(server.address().port, '127.0.0.1'); + existingSocket.once('connect', common.mustCall(() => { + process.accessControl.apply({ netOutgoing: false }); + + existingSocket.write('foo'); + existingSocket.setEncoding('utf8'); + existingSocket.once('data', common.mustCall((data) => { + assert.strictEqual(data, 'foo'); + existingSocket.end(); + server.close(); + })); + + net.connect(server.address().port, '127.0.0.1') + .once('error', common.expectsError({ + type: Error, + message: /connect EPERM 127\.0\.0\.1:\d+/, + code: 'EPERM' + })); + })); + })); +} + +{ + process.accessControl.apply({ netIncoming: false }); + + net.createServer().listen(0).once('error', common.expectsError({ + type: Error, + message: /listen EPERM 0\.0\.0\.0/, + code: 'EPERM' + })); +} + +{ + const writeStream = fs.createWriteStream( + path.join(tmpdir.path, 'writable-stream.txt')); + + process.accessControl.apply({ fsWrite: false }); + + common.expectsError(() => { fs.writeFileSync('foo.txt', 'blah'); }, { + type: Error, + message: 'Access to this API has been restricted', + code: 'ERR_ACCESS_DENIED' + }); + + // The 'open' event is emitted because permissions were still available + // originally, but... + writeStream.on('open', common.mustCall(() => { + // ... actually writing data is forbidden. + common.expectsError(() => { writeStream.write('X'); }, { + type: Error, + message: 'Access to this API has been restricted', + code: 'ERR_ACCESS_DENIED' + }); + })); +} + +{ + const readStream = fs.createReadStream(__filename); + + // The fs module kicks off reading automatically by calling .read(). + // We want to intercept the exception, so we have to wrap .read(): + readStream.read = common.mustCall(() => { + common.expectsError(() => { + fs.ReadStream.prototype.read.call(readStream); + }, { + type: Error, + message: 'Access to this API has been restricted', + code: 'ERR_ACCESS_DENIED' + }); + }); + + process.accessControl.apply({ fsRead: false }); + + common.expectsError(() => { fs.readFileSync('foo.txt'); }, { + type: Error, + message: 'Access to this API has been restricted', + code: 'ERR_ACCESS_DENIED' + }); + + common.expectsError(() => { require('nonexistent.js'); }, { + type: Error, + message: 'Access to this API has been restricted', + code: 'ERR_ACCESS_DENIED' + }); +} From b3cfaed28ca22962bf347312b681cec2bbbebe28 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 3 Aug 2018 22:39:23 +0200 Subject: [PATCH 2/3] worker: add feature access control API Similarly to the previous commit, this API allows prohibiting usage of certain features inside a worker thread. Refs: https://github.com/nodejs/node/issues/22107 --- doc/api/worker_threads.md | 13 +++++ lib/internal/worker.js | 6 +++ src/node_worker.cc | 17 ++++++ src/node_worker.h | 2 + test/parallel/test-worker-access-control.js | 57 +++++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 test/parallel/test-worker-access-control.js diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index 025a8c9a3ab2cf..0566b2d04d729c 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -305,6 +305,12 @@ if (isMainThread) { ``` ### new Worker(filename[, options]) + * `filename` {string} The path to the Worker’s main script. Must be either an absolute path or a relative path (i.e. relative to the @@ -312,6 +318,8 @@ if (isMainThread) { If `options.eval` is true, this is a string containing JavaScript code rather than a path. * `options` {Object} + * `accessControl` {Object} A set of access restrictions that will be applied + to the worker thread. See [`process.accessControl.apply()`][] for details. * `eval` {boolean} If true, interpret the first argument to the constructor as a script that is executed once the worker is online. * `workerData` {any} Any JavaScript value that will be cloned and made @@ -327,6 +335,10 @@ if (isMainThread) { * stderr {boolean} If this is set to `true`, then `worker.stderr` will not automatically be piped through to `process.stderr` in the parent. +Note that if `accessControl.fsRead` is set to `false`, the worker thread will +not be able to read its main script from the file system, so a source script +should be passed in instead and the `eval` option should be set. + ### Event: 'error' + +Disable certain features of Node.js at startup. See [`process.accessControl`][] +for more details. + ### `--enable-fips` + +Allow disabling certain features of Node.js at runtime. +See [`process.accessControl`][] for more details. +The `--disable` flag implies this flag. + ### `--experimental-modules`