Skip to content
50 changes: 50 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,53 @@ This behavior also applies to `child_process.spawn()`, but in that case, the
flags are propagated via the `NODE_OPTIONS` environment variable rather than
directly through the process arguments.

### `--allow-env`

> Stability: 1.1 - Active development

When using the [Permission Model][], access to environment variables is
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This paragraph and the console block below it describe a deny-by-default model that the code does not implement

./out/Release/node --permission '--allow-fs-read=*' -p 'process.env.HOME'
/Users/thisalihassan

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch! You're right, env vars are unrestricted by default with --permission. They only get restricted when --allow-env is explicitly passed. Updated both cli.md and process.md with the correct description and working examples.

unrestricted by default. To restrict access, the `--allow-env` flag must be
explicitly passed when starting Node.js. Once `--allow-env` is specified,
only the listed environment variables are accessible.

* Reading a restricted variable (`process.env.HOME`) silently returns
`undefined`.
* Writing (`process.env.FOO = 'bar'`) or deleting (`delete process.env.FOO`)
a restricted variable throws `ERR_ACCESS_DENIED`.
* Enumerating (`Object.keys(process.env)`) only returns allowed variables.

The valid arguments for the `--allow-env` flag are:

* `*` - To allow access to all environment variables.
* Specific environment variable names can be allowed using a comma-separated
list. Example: `--allow-env=HOME,PATH,NODE_ENV`

Examples:

```console
$ node --permission --allow-fs-read=* -p 'process.env.HOME'
/Users/user
$ node --permission --allow-env=PATH --allow-fs-read=* -p 'process.env.HOME'
undefined
$ node --permission --allow-env=HOME --allow-fs-read=* -p 'process.env.HOME'
/Users/user
```

Attempting to write a restricted variable throws:

```console
$ node --permission --allow-env=HOME --allow-fs-read=* -e "process.env.FOO = 'bar'"
node:internal/process/per_thread:12
throw new ERR_ACCESS_DENIED('EnvVar', name);
^

Error: Access to this API has been restricted
at node:internal/main/run_main_module:17:47 {
code: 'ERR_ACCESS_DENIED',
permission: 'EnvVar'
}
```

### `--allow-ffi`

<!-- YAML
Expand Down Expand Up @@ -2224,6 +2271,7 @@ following permissions are restricted:
* Worker Threads - manageable through [`--allow-worker`][] flag
* WASI - manageable through [`--allow-wasi`][] flag
* Addons - manageable through [`--allow-addons`][] flag
* Environment Variables - manageable through [`--allow-env`][] flag
* FFI - manageable through [`--allow-ffi`](#--allow-ffi) flag

### `--permission-audit`
Expand Down Expand Up @@ -3671,6 +3719,7 @@ one is included in the list below.

* `--allow-addons`
* `--allow-child-process`
* `--allow-env`
* `--allow-ffi`
* `--allow-fs-read`
* `--allow-fs-write`
Expand Down Expand Up @@ -4309,6 +4358,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`"type"`]: packages.md#type
[`--allow-addons`]: #--allow-addons
[`--allow-child-process`]: #--allow-child-process
[`--allow-env`]: #--allow-env
[`--allow-fs-read`]: #--allow-fs-read
[`--allow-fs-write`]: #--allow-fs-write
[`--allow-net`]: #--allow-net
Expand Down
4 changes: 3 additions & 1 deletion doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ using the [`--allow-child-process`][] and [`--allow-worker`][] respectively.
To allow network access, use [`--allow-net`][] and for allowing native addons
when using permission model, use the [`--allow-addons`][]
flag. For WASI, use the [`--allow-wasi`][] flag. For FFI, use the
[`--allow-ffi`][] flag. The [`node:ffi`](ffi.md) module also requires the
[`--allow-ffi`][] flag. For environment variables, use the
[`--allow-env`][] flag. The [`node:ffi`](ffi.md) module also requires the
`--experimental-ffi` flag and is only available in builds with FFI support.

#### Runtime API
Expand Down Expand Up @@ -283,6 +284,7 @@ Developers relying on --permission to sandbox untrusted code should be aware tha
[Security Policy]: https://github.com/nodejs/node/blob/main/SECURITY.md
[`--allow-addons`]: cli.md#--allow-addons
[`--allow-child-process`]: cli.md#--allow-child-process
[`--allow-env`]: cli.md#--allow-env
[`--allow-ffi`]: cli.md#--allow-ffi
[`--allow-fs-read`]: cli.md#--allow-fs-read
[`--allow-fs-write`]: cli.md#--allow-fs-write
Expand Down
9 changes: 9 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -1559,6 +1559,14 @@ changes:
The `process.env` property returns an object containing the user environment.
See environ(7).

When the [Permission Model][] is enabled, access to environment variables is
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same issue as cli.md

unrestricted by default. To restrict access, the `--allow-env` flag must be
explicitly passed. Once `--allow-env` is specified, reading a restricted
variable silently returns `undefined`, while writing or deleting throws
`ERR_ACCESS_DENIED`. Use `--allow-env=*` to grant access to all variables or
`--allow-env=HOME,PATH` to grant access to specific ones. See the
[`--allow-env`][] documentation for more details.

An example of this object looks like:

<!-- eslint-skip -->
Expand Down Expand Up @@ -4595,6 +4603,7 @@ cases:
[`'exit'`]: #event-exit
[`'message'`]: child_process.md#event-message
[`'uncaughtException'`]: #event-uncaughtexception
[`--allow-env`]: cli.md#--allow-env
[`--no-deprecation`]: cli.md#--no-deprecation
[`--permission`]: cli.md#--permission
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
Expand Down
23 changes: 23 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,25 @@ This behavior also applies to \fBchild_process.spawn()\fR, but in that case, the
flags are propagated via the \fBNODE_OPTIONS\fR environment variable rather than
directly through the process arguments.
.
.It Fl -allow-env
When using the Permission Model, the process will be able to access all
environment variables by default.
When the \fB--allow-env\fR flag is used, the process will only be able to access
the specified environment variables.
.Pp
The valid arguments for the \fB--allow-env\fR flag are:
.Bl -bullet
.It
\fB*\fR - To allow access to all environment variables.
.It
Multiple environment variable names can be allowed using multiple
\fB--allow-env\fR flags.
Example \fB--allow-env=HOME --allow-env=PATH\fR
.El
.Bd -literal
$ node --permission --allow-env=HOME --allow-fs-read=* index.js
.Ed
.
.It Fl -allow-ffi
When using the Permission Model, the process will not be able to use
\fBnode:ffi\fR by default.
Expand Down Expand Up @@ -1146,6 +1165,8 @@ WASI - manageable through \fB--allow-wasi\fR flag
Addons - manageable through \fB--allow-addons\fR flag
.It
FFI - manageable through \fB--allow-ffi\fR flag
.It
Environment Variables - manageable through \fB--allow-env\fR flag
.El
.
.It Fl -permission-audit
Expand Down Expand Up @@ -1860,6 +1881,8 @@ one is included in the list below.
.It
\fB--allow-child-process\fR
.It
\fB--allow-env\fR
.It
\fB--allow-ffi\fR
.It
\fB--allow-fs-read\fR
Expand Down
1 change: 1 addition & 0 deletions lib/internal/process/permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ module.exports = ObjectFreeze({
'--allow-fs-write',
'--allow-addons',
'--allow-child-process',
'--allow-env',
'--allow-net',
'--allow-inspector',
'--allow-wasi',
Expand Down
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
'src/path.cc',
'src/permission/child_process_permission.cc',
'src/permission/ffi_permission.cc',
'src/permission/env_var_permission.cc',
'src/permission/fs_permission.cc',
'src/permission/inspector_permission.cc',
'src/permission/permission.cc',
Expand Down Expand Up @@ -309,6 +310,7 @@
'src/path.h',
'src/permission/child_process_permission.h',
'src/permission/ffi_permission.h',
'src/permission/env_var_permission.h',
'src/permission/fs_permission.h',
'src/permission/inspector_permission.h',
'src/permission/permission.h',
Expand Down
5 changes: 5 additions & 0 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,11 @@ Environment::Environment(IsolateData* isolate_data,
permission()->Apply(this, {"*"}, permission::PermissionScope::kWASI);
}

if (!options_->allow_env.empty()) {
permission()->Apply(
this, options_->allow_env, permission::PermissionScope::kEnvVar);
}

// Implicit allow entrypoint to kFileSystemRead
if (!options_->has_eval_string && !options_->force_repl) {
std::string first_argv;
Expand Down
49 changes: 48 additions & 1 deletion src/node_env_var.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "node_external_reference.h"
#include "node_i18n.h"
#include "node_process-inl.h"
#include "permission/permission.h"
#include "util.h"

#include <time.h> // tzset(), _tzset()
Expand Down Expand Up @@ -435,6 +436,14 @@ static Intercepted EnvGetter(Local<Name> property,
return Intercepted::kYes;
}
CHECK(property->IsString());

Utf8Value key(env->isolate(), property);
if (env->permission()->enabled() &&
!env->permission()->is_granted(
env, permission::PermissionScope::kEnvVar, key.ToStringView())) {
return Intercepted::kNo;
}

MaybeLocal<String> value_string =
env->env_vars()->Get(env->isolate(), property.As<String>());

Expand All @@ -453,6 +462,15 @@ static Intercepted EnvSetter(Local<Name> property,
const PropertyCallbackInfo<void>& info) {
Environment* env = Environment::GetCurrent(info);
CHECK(env->has_run_bootstrapping_code());

if (property->IsString()) {
Utf8Value key(env->isolate(), property);
THROW_IF_INSUFFICIENT_PERMISSIONS(env,
permission::PermissionScope::kEnvVar,
key.ToStringView(),
Intercepted::kYes);
}

// calling env->EmitProcessEnvWarning() sets a variable indicating that
// warnings have been emitted. It should be called last after other
// conditions leading to a warning have been met.
Expand Down Expand Up @@ -489,6 +507,13 @@ static Intercepted EnvQuery(Local<Name> property,
Environment* env = Environment::GetCurrent(info);
CHECK(env->has_run_bootstrapping_code());
if (property->IsString()) {
Utf8Value key(env->isolate(), property);
if (env->permission()->enabled() &&
!env->permission()->is_granted(
env, permission::PermissionScope::kEnvVar, key.ToStringView())) {
return Intercepted::kNo;
}

int32_t rc = env->env_vars()->Query(env->isolate(), property.As<String>());
bool has_env = (rc != -1);
TraceEnvVar(env, "query", property.As<String>());
Expand All @@ -506,6 +531,12 @@ static Intercepted EnvDeleter(Local<Name> property,
Environment* env = Environment::GetCurrent(info);
CHECK(env->has_run_bootstrapping_code());
if (property->IsString()) {
Utf8Value key(env->isolate(), property);
THROW_IF_INSUFFICIENT_PERMISSIONS(env,
permission::PermissionScope::kEnvVar,
key.ToStringView(),
Intercepted::kYes);

env->env_vars()->Delete(env->isolate(), property.As<String>());

TraceEnvVar(env, "delete", property.As<String>());
Expand All @@ -525,7 +556,23 @@ static void EnvEnumerator(const PropertyCallbackInfo<Array>& info) {

Local<Array> ret;
if (env->env_vars()->Enumerate(env->isolate()).ToLocal(&ret)) {
info.GetReturnValue().Set(ret);
if (env->permission()->enabled()) {
LocalVector<Value> filtered(env->isolate());
for (uint32_t i = 0; i < ret->Length(); i++) {
Local<Value> elem;
if (!ret->Get(env->context(), i).ToLocal(&elem)) continue;
Utf8Value key(env->isolate(), elem);
if (env->permission()->is_granted(env,
permission::PermissionScope::kEnvVar,
key.ToStringView())) {
filtered.push_back(elem);
}
}
info.GetReturnValue().Set(
Array::New(env->isolate(), filtered.data(), filtered.size()));
} else {
info.GetReturnValue().Set(ret);
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
kAllowedInEnvvar,
false,
OptionNamespaces::kPermissionNamespace);
AddOption("--allow-env",
"allow access to environment variables when any permissions are "
"set",
&EnvironmentOptions::allow_env,
kAllowedInEnvvar,
OptionNamespaces::kPermissionNamespace);
AddOption("--experimental-repl-await",
"experimental await keyword support in REPL",
&EnvironmentOptions::experimental_repl_await,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ class EnvironmentOptions : public Options {
bool allow_wasi = false;
bool allow_ffi = false;
bool allow_worker_threads = false;
std::vector<std::string> allow_env;
bool experimental_repl_await = true;
bool experimental_vm_modules = false;
bool async_context_frame = true;
Expand Down
42 changes: 42 additions & 0 deletions src/permission/env_var_permission.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#include "env_var_permission.h"

#include <string>
#include <vector>

namespace node {

namespace permission {

void EnvVarPermission::Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) {
deny_all_ = true;
if (!allow.empty()) {
if (allow.size() == 1 && allow[0] == "*") {
allow_all_ = true;
return;
}
for (const std::string& arg : allow) {
allowed_env_vars_.insert(arg);
}
}
}

bool EnvVarPermission::is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const {
if (deny_all_) {
if (allow_all_) {
return true;
}
if (param.empty()) {
return false;
}
return allowed_env_vars_.find(std::string(param)) !=
allowed_env_vars_.end();
}
return true;
}

} // namespace permission
} // namespace node
35 changes: 35 additions & 0 deletions src/permission/env_var_permission.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#ifndef SRC_PERMISSION_ENV_VAR_PERMISSION_H_
#define SRC_PERMISSION_ENV_VAR_PERMISSION_H_

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include <set>
#include <string>
#include <vector>
#include "permission/permission_base.h"

namespace node {

namespace permission {

class EnvVarPermission final : public PermissionBase {
public:
void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) override;
bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param = "") const override;

private:
bool deny_all_ = false;
bool allow_all_ = false;
std::set<std::string> allowed_env_vars_;
};

} // namespace permission

} // namespace node

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_PERMISSION_ENV_VAR_PERMISSION_H_
Loading
Loading