Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

process: initial impl of feature access control #22112

Closed
wants to merge 3 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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
addaleax committed Aug 19, 2018
commit 85373119c0757634d237733f85b5d51ee384b8bb
9 changes: 9 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
@@ -578,6 +578,14 @@ found [here][online].
<a id="nodejs-error-codes"></a>
## Node.js Error Codes

<a id="ERR_ACCESS_DENIED"></a>
### 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.

<a id="ERR_AMBIGUOUS_ARGUMENT"></a>
### 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
156 changes: 156 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
@@ -1996,6 +1996,141 @@ Will generate an object similar to:
tz: '2016b' }
```

## Access Control

> Stability: 1 - Experimental
### process.accessControl.apply(restrictions)
<!-- YAML
added: REPLACEME
-->

* `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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be clarified. For instance, if I use fs.createWriteStream() now to open a file for writing, then set the access control policy, will I be able to use the write stream or no?

Another question: if the Node.js process stdout/stderr are redirected to a pipe and fsWrite is denied, what happens when the process attempts to write out?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasnell This is a very good point.

For instance, if I use fs.createWriteStream() now to open a file for writing, then set the access control policy, will I be able to use the write stream or no?

You will not, because of the way fs streams are implemented – as individual fs.read and fs.write operations in JS.

However, existing network sockets can still be used, because we implement them in a very abstract way in C++ that is not specific to the kind of resource (e.g. net & tty all use the same code). We can align behaviours if we think that that makes sense, though.

I’ve pushed docs & test changes to account for these scenarios.

Another question: if the Node.js process stdout/stderr are redirected to a pipe and fsWrite is denied, what happens when the process attempts to write out?

Assuming you meant “redirected to a file”: An exception occurs. console.log and friends silence those, but using raw process.stdout.write() will throw.


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()
<!-- YAML
added: REPLACEME
-->

* Returns: {Object} An object representing a set of permissions for the
current Node.js instance of the following structure:

<!-- eslint-skip -->
```js
{ accessInspectorBindings: true,
Copy link
Member

@benjamingr benjamingr Aug 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing some stuff like http2 and dns? (dns is under net outgoing?)

Copy link
Member Author

@addaleax addaleax Aug 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dns currently falls under netOutgoing, but I missed that in the doc here (updated with that).

HTTP/2 is a purely computational thing, so I don’t think there’s anything to restrict here (although that could of course be implemented)? For now, that’s also covered by the net flags

childProcesses: true,

This comment was marked as resolved.

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:
Copy link
Contributor

@davisjam davisjam Aug 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find fsRead and fsWrite a bit confusing, since there are already APIs of this name.

Since this is for access control, perhaps we should use the naming convention <verb> [<preposition>] <noun>, e.g. writeToFs and readFromFs. This would affect the childProcesses, the net*, and perhaps others.

Thoughts on a consistent convention for "write" vs. "read" operations. e.g. setProcessAttrs -> changeProcessAttrs, fsWrite -> changeFs, that kind of thing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, interesting … is the only reason for this that these names refer to specific functions? The current naming scheme seems a bit more useful if we have multiple keys that we want to appear as groups when sorted alphabetically (e.g. net*).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the only reason for this that these names refer to specific functions?

In the specific case of fsRead and fsWrite it is confusing, yes.

But keeping naming consistent within an API helps users. Especially since you say:

Keys not listed in the object, or with values not set to `false`, are unaffected.

This means if I mis-type a name then I will have a false sense of security. I would rather have a little verbosity than a security issue.

if we have multiple keys that we want to appear as groups

I imagine this being useful primarily for APIs in the same module. You might address that with an increasingly verbose naming convention <module>_<verb>[<preposition>]<noun>. Are there cases where you would want to group keys that are part of different modules?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So … what’s the noun for fs then? :) fs_writeFilesystem? 😄

My gut still says that we want something a) short/succinct and b) with the module coming first.

Maybe fsObserve/fsModify?


- `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`:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be worthwhile to remark on open explicitly, since it's a vectored API whose behavior depends on the flags (and the access check looks at the flags)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, makes sense. Done!

- 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: both after [`setTimeout(fn, 0)`] ?

[`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
2 changes: 2 additions & 0 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
@@ -98,6 +98,8 @@
perThreadSetup.setupMemoryUsage(_memoryUsage);
perThreadSetup.setupKillAndExit();

process.accessControl = internalBinding('access_control');

if (global.__coverage__)
NativeModule.require('internal/process/write-coverage').setup();

1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
@@ -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',
3 changes: 3 additions & 0 deletions src/cares_wrap.cc
Original file line number Diff line number Diff line change
@@ -1800,6 +1800,7 @@ class GetHostByAddrWrap: public QueryWrap {
template <class Wrap>
static void Query(const FunctionCallbackInfo<Value>& 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<Value>& args) {

void GetAddrInfo(const FunctionCallbackInfo<Value>& 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<Value>& args) {

void GetNameInfo(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, netOutgoing);

CHECK(args[0]->IsObject());
CHECK(args[1]->IsString());
25 changes: 25 additions & 0 deletions src/env-inl.h
Original file line number Diff line number Diff line change
@@ -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<size_t>(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<size_t>(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_;
}
57 changes: 57 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@
#include "node.h"
#include "node_http2_state.h"

#include <bitset>
#include <list>
#include <stdint.h>
#include <vector>
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason this is defined as an enum and not enum class?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gioragutt Nothing, besides the fact that it creates an extra scope inside what is already a scope, so accessing it means a bit more stuff to write out.

(One could move it one layer up into the node namespace, but … feels right here to me?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I guess that since this is not the global scope, this is fine.
I just have traumas from working with global scoped non-class enums 😅

#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<v8::Object> ToObject(v8::Local<v8::Context> context);
static v8::Maybe<AccessControl> FromObject(v8::Local<v8::Context> context,
v8::Local<v8::Object> object);

static void ThrowAccessDenied(Environment* env, Permission perm);

private:
std::bitset<kNumPermissions> 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_;
1 change: 1 addition & 0 deletions src/fs_event_wrap.cc
Original file line number Diff line number Diff line change
@@ -134,6 +134,7 @@ void FSEventWrap::New(const FunctionCallbackInfo<Value>& args) {
// wrap.start(filename, persistent, recursive, encoding)
void FSEventWrap::Start(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead);

FSEventWrap* wrap = Unwrap<FSEventWrap>(args.This());
CHECK_NOT_NULL(wrap);
7 changes: 6 additions & 1 deletion src/inspector_js_api.cc
Original file line number Diff line number Diff line change
@@ -75,6 +75,7 @@ class JSBindingsConnection : public AsyncWrap {

static void New(const FunctionCallbackInfo<Value>& info) {
Environment* env = Environment::GetCurrent(info);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, accessInspectorBindings);
CHECK(info[0]->IsFunction());
Local<Function> callback = info[0].As<Function>();
new JSBindingsConnection(env, info.This(), callback);
@@ -122,7 +123,8 @@ static bool InspectorEnabled(Environment* env) {
}

void AddCommandLineAPI(const FunctionCallbackInfo<Value>& info) {
auto env = Environment::GetCurrent(info);
Environment* env = Environment::GetCurrent(info);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, accessInspectorBindings);
Local<Context> context = env->context();

// inspector.addCommandLineAPI takes 2 arguments: a string and a value.
@@ -135,6 +137,7 @@ void AddCommandLineAPI(const FunctionCallbackInfo<Value>& info) {

void CallAndPauseOnStart(const FunctionCallbackInfo<v8::Value>& 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<Value>& args) {

void Open(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, accessInspectorBindings);

Agent* agent = env->inspector_agent();
bool wait_for_connect = false;

2 changes: 2 additions & 0 deletions src/module_wrap.cc
Original file line number Diff line number Diff line change
@@ -640,6 +640,7 @@ Maybe<URL> Resolve(Environment* env,
const std::string& specifier,
const URL& base,
PackageMainCheck check_pjson_main) {
THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead, Nothing<URL>());
URL pure_url(specifier);
if (!(pure_url.flags() & URL_FLAGS_FAILED)) {
// just check existence, without altering
@@ -668,6 +669,7 @@ Maybe<URL> Resolve(Environment* env,

void ModuleWrap::Resolve(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead);

// module.resolve(specifier, url)
CHECK_EQ(args.Length(), 2);
15 changes: 15 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
@@ -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<Value>& 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<Name> property,
static void ProcessTitleSetter(Local<Name> property,
Local<Value> value,
const PropertyCallbackInfo<void>& 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<Name> property,
Local<Value> value,
const PropertyCallbackInfo<Value>& 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<Name> property,

static void EnvDeleter(Local<Name> property,
const PropertyCallbackInfo<Boolean>& 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<Name> property,
static void DebugPortSetter(Local<Name> property,
Local<Value> value,
const PropertyCallbackInfo<void>& 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<Value>& 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<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, signalProcesses);

Isolate* isolate = args.GetIsolate();
DWORD pid;
HANDLE process = nullptr;
123 changes: 123 additions & 0 deletions src/node_access_control.cc
Original file line number Diff line number Diff line change
@@ -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<Object> AccessControl::ToObject(Local<Context> context) {
Isolate* isolate = context->GetIsolate();
EscapableHandleScope handle_scope(isolate);

Local<Object> 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<Object>(); \
}
ACCESS_CONTROL_FLAGS(V)
#undef V

return handle_scope.Escape(obj);
}

Maybe<AccessControl> AccessControl::FromObject(Local<Context> context,
Local<Object> obj) {
Isolate* isolate = context->GetIsolate();
HandleScope scope(isolate);

AccessControl ret;

Local<Value> field;
#define V(kind) \
if (!obj->Get(context, FIXED_ONE_BYTE_STRING(isolate, #kind)) \
.ToLocal(&field)) { \
return Nothing<AccessControl>(); \
} \
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<Value> err = ERR_ACCESS_DENIED(env->isolate());
CHECK(err->IsObject());
err.As<Object>()->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<Value>& args) {
Environment* env = Environment::GetCurrent(args);

AccessControl access_control;
Local<Object> 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<Value>& args) {
Environment* env = Environment::GetCurrent(args);

Local<Object> ret;
if (env->access_control()->ToObject(env->context()).ToLocal(&ret))
args.GetReturnValue().Set(ret);
}

void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> 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)
2 changes: 2 additions & 0 deletions src/node_errors.h
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ namespace node {
// a `Local<Value>` 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`") \
46 changes: 44 additions & 2 deletions src/node_file.cc
Original file line number Diff line number Diff line change
@@ -285,6 +285,8 @@ FileHandleReadWrap::FileHandleReadWrap(FileHandle* handle, Local<Object> 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> value,

void Access(const FunctionCallbackInfo<Value>& 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<Value>& args) {
// in the file.
static void InternalModuleReadJSON(const FunctionCallbackInfo<Value>& 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<Value>& args) {
// The speedup comes from not creating thousands of Stat and Error objects.
static void InternalModuleStat(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void Stat(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void LStat(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void FStat(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void Symlink(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void Link(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void ReadLink(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void Rename(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void FTruncate(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void Fdatasync(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void Fsync(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void Unlink(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void RMDir(const FunctionCallbackInfo<Value>& 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<Value>& 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<Value>& args) {

static void RealPath(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void ReadDir(const FunctionCallbackInfo<Value>& 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<Value>& args) {
CHECK(args[1]->IsInt32());
const int flags = args[1].As<Int32>()->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<Int32>()->Value();

@@ -1616,6 +1641,11 @@ static void OpenFileHandle(const FunctionCallbackInfo<Value>& args) {
CHECK(args[1]->IsInt32());
const int flags = args[1].As<Int32>()->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<Int32>()->Value();

@@ -1641,6 +1671,7 @@ static void OpenFileHandle(const FunctionCallbackInfo<Value>& args) {

static void CopyFile(const FunctionCallbackInfo<Value>& 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<Value>& args) {
// if null, write from the current position
static void WriteBuffer(const FunctionCallbackInfo<Value>& 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<Value>& args) {
// if null, write from the current position
static void WriteBuffers(const FunctionCallbackInfo<Value>& 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<Value>& args) {
// 3 enc encoding of string
static void WriteString(const FunctionCallbackInfo<Value>& 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<Value>& args) {
*/
static void Read(const FunctionCallbackInfo<Value>& 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<Value>& args) {
*/
static void Chmod(const FunctionCallbackInfo<Value>& 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<Value>& args) {
*/
static void FChmod(const FunctionCallbackInfo<Value>& 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<Value>& args) {
*/
static void Chown(const FunctionCallbackInfo<Value>& 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<Value>& args) {
*/
static void FChown(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void LChown(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void UTimes(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void FUTimes(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void Mkdtemp(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsWrite);

const int argc = args.Length();
CHECK_GE(argc, 2);
1 change: 1 addition & 0 deletions src/node_internals.h
Original file line number Diff line number Diff line change
@@ -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) \
2 changes: 2 additions & 0 deletions src/node_os.cc
Original file line number Diff line number Diff line change
@@ -321,6 +321,7 @@ static void GetInterfaceAddresses(const FunctionCallbackInfo<Value>& args) {

static void GetHomeDirectory(const FunctionCallbackInfo<Value>& 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<Value>& args) {

static void GetUserInfo(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead);
uv_passwd_t pwd;
enum encoding encoding;

8 changes: 8 additions & 0 deletions src/node_process.cc
Original file line number Diff line number Diff line change
@@ -60,6 +60,7 @@ void Abort(const FunctionCallbackInfo<Value>& args) {

void Chdir(const FunctionCallbackInfo<Value>& 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<Value>& args) {

void Kill(const FunctionCallbackInfo<Value>& 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<Value>& args) {
void SetGid(const FunctionCallbackInfo<Value>& 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<Value>& args) {
void SetEGid(const FunctionCallbackInfo<Value>& 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<Value>& args) {

void SetUid(const FunctionCallbackInfo<Value>& 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<Value>& args) {

void SetEUid(const FunctionCallbackInfo<Value>& 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<Value>& args) {

void SetGroups(const FunctionCallbackInfo<Value>& 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<Value>& args) {

void InitGroups(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, setProcessAttrs);

CHECK_EQ(args.Length(), 2);
CHECK(args[0]->IsUint32() || args[0]->IsString());
3 changes: 3 additions & 0 deletions src/node_stat_watcher.cc
Original file line number Diff line number Diff line change
@@ -97,11 +97,14 @@ void StatWatcher::Callback(uv_fs_poll_t* handle,
void StatWatcher::New(const FunctionCallbackInfo<Value>& 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<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, fsRead);
CHECK_EQ(args.Length(), 2);

StatWatcher* wrap;
2 changes: 2 additions & 0 deletions src/node_trace_events.cc
Original file line number Diff line number Diff line change
@@ -63,6 +63,7 @@ void NodeCategorySet::New(const FunctionCallbackInfo<Value>& args) {

void NodeCategorySet::Enable(const FunctionCallbackInfo<Value>& 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<Value>& args) {

void NodeCategorySet::Disable(const FunctionCallbackInfo<Value>& 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);
3 changes: 3 additions & 0 deletions src/node_v8.cc
Original file line number Diff line number Diff line change
@@ -111,6 +111,9 @@ void UpdateHeapSpaceStatisticsBuffer(const FunctionCallbackInfo<Value>& args) {


void SetFlagsFromString(const FunctionCallbackInfo<Value>& 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());
2 changes: 2 additions & 0 deletions src/node_worker.cc
Original file line number Diff line number Diff line change
@@ -103,6 +103,7 @@ Worker::Worker(Environment* env, Local<Object> 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<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, createWorkers);

CHECK(args.IsConstructCall());

5 changes: 5 additions & 0 deletions src/pipe_wrap.cc
Original file line number Diff line number Diff line change
@@ -163,6 +163,7 @@ PipeWrap::PipeWrap(Environment* env,
void PipeWrap::Bind(const FunctionCallbackInfo<Value>& 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<Value>& args) {
void PipeWrap::Fchmod(const v8::FunctionCallbackInfo<v8::Value>& 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<Int32>()->Value();
int err = uv_pipe_chmod(reinterpret_cast<uv_pipe_t*>(&wrap->handle_),
@@ -193,6 +195,7 @@ void PipeWrap::Fchmod(const v8::FunctionCallbackInfo<v8::Value>& args) {
void PipeWrap::Listen(const FunctionCallbackInfo<Value>& 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<uv_stream_t*>(&wrap->handle_),
backlog,
@@ -206,6 +209,7 @@ void PipeWrap::Open(const FunctionCallbackInfo<Value>& 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<Value>& 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());
5 changes: 5 additions & 0 deletions src/process_wrap.cc
Original file line number Diff line number Diff line change
@@ -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<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, childProcesses);

Local<Context> 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<Value>& 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();
1 change: 1 addition & 0 deletions src/spawn_sync.cc
Original file line number Diff line number Diff line change
@@ -370,6 +370,7 @@ void SyncProcessRunner::Initialize(Local<Object> target,

void SyncProcessRunner::Spawn(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
THROW_IF_INSUFFICIENT_PERMISSIONS(env, childProcesses);
env->PrintSyncTrace();
SyncProcessRunner p(env);
Local<Value> result = p.Run(args[0]);
41 changes: 41 additions & 0 deletions src/tcp_wrap.cc
Original file line number Diff line number Diff line change
@@ -204,6 +204,14 @@ void TCPWrap::Open(const FunctionCallbackInfo<Value>& 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<int>(args[0]->IntegerValue());
int err = uv_tcp_open(&wrap->handle_, fd);

@@ -219,6 +227,13 @@ void TCPWrap::Bind(const FunctionCallbackInfo<Value>& 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<Value>& 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<Value>& 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<uv_stream_t*>(&wrap->handle_),
backlog,
@@ -271,6 +300,12 @@ void TCPWrap::Connect(const FunctionCallbackInfo<Value>& 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<Value>& 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());
4 changes: 4 additions & 0 deletions src/udp_wrap.cc
Original file line number Diff line number Diff line change
@@ -175,6 +175,7 @@ void UDPWrap::DoBind(const FunctionCallbackInfo<Value>& 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<Value>& 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<Value>& 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)
159 changes: 159 additions & 0 deletions test/parallel/test-access-control.js
Original file line number Diff line number Diff line change
@@ -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'
});
}