Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
31 changes: 30 additions & 1 deletion benchmark/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,21 @@ class Benchmark {
}

_run() {
// A forked child is told to run the benchmark function directly, rather
// than build its own queue and fork again, through the
// NODE_RUN_BENCHMARK_FN environment variable. A child always inherits
// this.flags in its execArgv, so reaching _run() with those flags already
// applied means the variable did not survive to the child and every
// generation would keep forking. Fail loudly instead of forking forever.
if (process.send &&
this.flags.length > 0 &&
this.flags.every((flag) => process.execArgv.includes(flag))) {
throw new Error(
'Benchmark child process was started with the benchmark flags but ' +
'without NODE_RUN_BENCHMARK_FN, refusing to fork again. Something ' +
'removed the variable from the child environment.');
}

// If forked, report to the parent.
if (process.send) {
process.send({
Expand All @@ -213,6 +228,20 @@ class Benchmark {
this.originalOptions.setup(this.queue);
}

// Enforcing the permission model removes the environment variables
// --allow-env does not grant access to at startup, which would drop the
// NODE_RUN_BENCHMARK_FN set below. The child only ever sees the
// environment this process hands it, so granting access to all of it does
// not widen what the benchmark can reach. Audit mode removes nothing, so
// it is left alone to keep its diagnostics intact.
const childExecArgv = this.flags.concat(process.execArgv);
const enforcesPermission = (arg) =>
arg === '--permission' || arg.startsWith('--permission=');
if (childExecArgv.some(enforcesPermission) &&
!childExecArgv.some((arg) => arg.startsWith('--allow-env'))) {
childExecArgv.push('--allow-env=*');
}

const recursive = (queueIndex) => {
const config = this.queue[queueIndex];

Expand All @@ -233,7 +262,7 @@ class Benchmark {

const child = child_process.fork(require.main.filename, childArgs, {
env: childEnv,
execArgv: this.flags.concat(process.execArgv),
execArgv: childExecArgv,
});
child.on('message', sendResult);
child.on('close', (code) => {
Expand Down
48 changes: 48 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,50 @@ 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`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active development

When using the [Permission Model][], the process starts without the environment
variables it has not been granted access to. At startup, every variable that
`--allow-env` does not match is removed from the process environment. Removed
variables are absent from `process.env`, from diagnostic reports, from native
code calling `getenv()`, and from the environment of child processes and worker
threads.

The valid values are:

* `*` - Grants access to every environment variable.
* A variable name, for example `--allow-env=DATABASE_URL`.
* A variable name prefix followed by `*`, for example `--allow-env=APP_*`.

Multiple values can be passed by repeating the flag, or by separating them with
commas: `--allow-env=PORT,APP_*`. Variable names are case-insensitive on
Windows.

Example:

```js
console.log(process.env.DATABASE_URL);
console.log(process.env.AWS_SECRET_ACCESS_KEY);
```

```console
$ node --permission --allow-fs-read=* --allow-env=DATABASE_URL index.js
postgres://localhost/app
undefined
(node:1234) Warning: The permission model removed the environment variable "AWS_SECRET_ACCESS_KEY" at startup. Use --allow-env to manage permissions.
```

The variables that Node.js and its bundled dependencies read, such as
`NODE_OPTIONS`, `PATH`, `HOME`, `TZ`, and `SSL_CERT_FILE`, are always kept, as
are the variables defined in [`--env-file`][] files. See
[Environment variable permissions][] for details.

### `--allow-ffi`

<!-- YAML
Expand Down Expand Up @@ -2538,6 +2582,7 @@ following permissions are restricted:
* File System - manageable through
[`--allow-fs-read`][], [`--allow-fs-write`][] flags
* Network - manageable through [`--allow-net`][] flag
* Environment variables - manageable through [`--allow-env`][] flag
* Child Process - manageable through [`--allow-child-process`][] flag
* Worker Threads - manageable through [`--allow-worker`][] flag
* WASI - manageable through [`--allow-wasi`][] flag
Expand Down Expand Up @@ -4128,6 +4173,7 @@ one is included in the list below.

* `--allow-addons`
* `--allow-child-process`
* `--allow-env`
* `--allow-ffi`
* `--allow-fs-read`
* `--allow-fs-vfs`
Expand Down Expand Up @@ -4776,6 +4822,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[CommonJS module]: modules.md
[DEP0025 warning]: deprecations.md#dep0025-requirenodesys
[ECMAScript module]: esm.md#modules-ecmascript-modules
[Environment variable permissions]: permissions.md#environment-variable-permissions
[EventSource Web API]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
[ExperimentalWarning: `vm.measureMemory` is an experimental feature]: vm.md#vmmeasurememoryoptions
[FIPS mode]: crypto.md#fips-mode
Expand All @@ -4799,6 +4846,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`'crypto.fips.indicator'`]: diagnostics_channel.md#event-cryptofipsindicator
[`--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
47 changes: 47 additions & 0 deletions doc/api/embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,51 @@ int main(int argc, char** argv) {
}
```

### Restricting access to environment variables

<!-- YAML
added: REPLACEME
-->

When the arguments passed to `node::InitializeOncePerProcess()` enable the
[Permission Model][] without `--allow-env=*`, the process environment must not
contain any variable that [`--allow-env`][] does not grant access to.
`node::InitializeOncePerProcess()` fails otherwise. Unlike the `node`
executable, embedders own the process environment, so Node.js does not remove
these variables itself.

`node::ScrubProcessEnvironment()` removes them. Because it modifies the process
environment without any locking that native code calling `getenv()`
participates in, it must be called before starting any thread that may read the
environment, and before `node::InitializeOncePerProcess()`:

```cpp
int main(int argc, char** argv) {
argv = uv_setup_args(argc, argv);
std::vector<std::string> args(argv, argv + argc);

// Keep the variables the embedder itself reads, in addition to the ones
// Node.js reads (see node::GetRuntimeEnvironmentDefaults()).
node::ProcessEnvironmentScrubOptions scrub_options;
scrub_options.allow = {"PORT", "APP_*"};
if (node::ScrubProcessEnvironment(scrub_options).IsNothing()) {
return 1;
}

// args contains, for example, --permission --allow-env=PORT
std::unique_ptr<node::InitializationResult> result =
node::InitializeOncePerProcess(args, {
node::ProcessInitializationFlags::kNoInitializeV8,
node::ProcessInitializationFlags::kNoInitializeNodeV8Platform
});
// ...
}
```

`process.permission.drop('env', name)` removes a variable from the process
environment, so it throws when called from a `node::Environment` created
without `node::EnvironmentFlags::kOwnsProcessState`.

### Setting up a per-instance state

<!-- YAML
Expand Down Expand Up @@ -178,6 +223,8 @@ int RunNodeInstance(MultiIsolatePlatform* platform,
```

[CLI options]: cli.md
[Permission Model]: permissions.md#permission-model
[`--allow-env`]: cli.md#--allow-env
[`process.memoryUsage()`]: process.md#processmemoryusage
[deprecation policy]: deprecations.md
[embedtest.cc]: https://github.com/nodejs/node/blob/HEAD/test/embedding/embedtest.cc
Expand Down
108 changes: 102 additions & 6 deletions doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ The Permission Model has two operational modes:

When starting Node.js with `--permission`,
the ability to access the file system through the `fs` module, access the network,
spawn processes, use `node:worker_threads`, use native addons, use WASI, use
FFI, and enable the runtime inspector will be restricted (the listener for
SIGUSR1 won't be created).
access environment variables, spawn processes, use `node:worker_threads`, use
native addons, use WASI, use FFI, and enable the runtime inspector will be
restricted (the listener for SIGUSR1 won't be created).

```console
$ node --permission index.js
Expand All @@ -79,6 +79,8 @@ Error: Access to this API has been restricted
Allowing access to spawning a process and creating worker threads can be done
using the [`--allow-child-process`][] and [`--allow-worker`][] respectively.

To grant access to environment variables, use [`--allow-env`][].

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
Expand Down Expand Up @@ -157,9 +159,9 @@ mode. Execution continues normally.
Audit mode is useful for discovering what permissions your application
requires before deploying with [`--permission`][]. It can also be combined
with the [`--allow-fs-read`][], [`--allow-fs-write`][], [`--allow-net`][],
[`--allow-child-process`][], [`--allow-worker`][], [`--allow-addons`][],
[`--allow-wasi`][], and [`--allow-ffi`][] flags to audit a subset of
permissions while granting others.
[`--allow-env`][], [`--allow-child-process`][], [`--allow-worker`][],
[`--allow-addons`][], [`--allow-wasi`][], and [`--allow-ffi`][] flags to audit
a subset of permissions while granting others.

When a permission check fails in audit mode, a message is published to the
diagnostics channel corresponding to the denied scope. The channel names are:
Expand All @@ -172,6 +174,7 @@ diagnostics channel corresponding to the denied scope. The channel names are:
* `node:permission-model:wasi` — WASI
* `node:permission-model:addon` — Native Addons
* `node:permission-model:ffi` — FFI
* `node:permission-model:env` — Environment variables

Each message is an object with the following properties:

Expand Down Expand Up @@ -266,6 +269,78 @@ both to the top-level `node:fs` functions and to the equivalent
`FileHandle` methods, and currently includes `fsync`/`fdatasync`,
`fchmod`, and `fchown` (and their synchronous variants).

#### Environment variable permissions

When the Permission Model is enforced, the process only has access to the
environment variables that [`--allow-env`][] grants access to.

Instead of checking each access, Node.js removes every other variable from the
process environment at startup, before any JavaScript code runs and before
Node.js starts any other thread. Removed variables are absent from everything
that exposes the environment of the process: `process.env`, diagnostic reports,
native code calling `getenv()`, worker threads, and the environment inherited by
child processes.

```console
$ node --permission --allow-env=PORT --allow-env=APP_* index.js
```

The valid arguments for the flag are:

* `*` - Grants access to every environment variable. Nothing is removed.
* A variable name, such as `PORT`.
* A variable name prefix followed by `*`, such as `APP_*`.

Some variables are always kept:

* The variables that Node.js and its bundled dependencies read after startup,
such as `NODE_OPTIONS`, `NODE_EXTRA_CA_CERTS`, `PATH`, `HOME`, `TMPDIR`, `TZ`,
`LANG`, `SSL_CERT_FILE`, and the variables that terminal color detection
reads. Other variables whose names start with `NODE_`, such as
`NODE_AUTH_TOKEN`, are not kept.
* The variables defined in the files passed to [`--env-file`][] and
[`--env-file-if-exists`][]. If a variable is defined in such a file and also
inherited from the parent process, and `--allow-env` does not grant access to
it, the inherited value is removed and the value from the file is used.

Proxy URLs often contain credentials, so the `HTTP_PROXY`, `HTTPS_PROXY`, and
`NO_PROXY` variables are not kept. Grant access to them explicitly when using
[`--use-env-proxy`][].

Reading a variable that was removed at startup returns `undefined`, emits a
warning the first time, and publishes a message to the
`node:permission-model:env` diagnostics channel.

Variables set at runtime, for example with `process.env.KEY = 'value'` or
[`process.loadEnvFile()`][], are not restricted, as they cannot reveal what was
removed.

Dropping a variable with [`permission.drop()`][] removes it from the
environment. Dropping the whole `env` scope removes every variable except the
ones Node.js reads itself. This makes it possible to read a secret during
initialization, and then remove it:

```js
const databaseUrl = process.env.DATABASE_URL;
process.permission.drop('env', 'DATABASE_URL');
```

When a process that enforces the Permission Model spawns a child process, the
child is started with `--allow-env=*`: the environment it inherits only contains
variables that the parent had access to.

In audit mode, nothing is removed. Accesses to variables that `--allow-env`
does not grant access to are published to the `node:permission-model:env`
diagnostics channel instead.

On Linux, `/proc/<pid>/environ` exposes the environment a process was started
with. While access to environment variables is restricted, reading any
`/proc/<pid>/environ` file is denied, regardless of [`--allow-fs-read`][], and
the removed variables are overwritten in the initial environment block of the
process. This does not affect the environment of other processes, such as the
parent process. A process granted [`--allow-child-process`][] can read their
environment through other programs.

#### Configuration file support

In addition to passing permission flags on the command line, they can also be
Expand Down Expand Up @@ -297,6 +372,20 @@ automatically enables the `--permission` flag. Run with:
$ node --experimental-default-config-file app.js
```

A configuration file, like the `NODE_OPTIONS` defined in an [`--env-file`][]
file, may be controlled by the project being run rather than by whoever starts
Node.js. When the command line or the `NODE_OPTIONS` environment variable
enable the Permission Model, the `allow-env` values these files define can only
narrow the access that [`--allow-env`][] grants, and never widen it:

```console
$ node --permission --allow-env=APP_* --experimental-config-file=node.config.json app.js
```

With `"allow-env": ["*"]` in `node.config.json`, only the variables starting with
`APP_` are kept. With `"allow-env": ["APP_DATABASE_URL", "OTHER"]`, only
`APP_DATABASE_URL` is.

#### Using the Permission Model with `npx`

If you're using [`npx`][] to execute a Node.js script, you can enable the
Expand Down Expand Up @@ -342,6 +431,7 @@ There are constraints you need to know before using this system:
* When using the Permission Model the following features will be restricted:
* Native modules
* Network
* Environment variables
* Child process
* Worker Threads
* Inspector protocol
Expand Down Expand Up @@ -404,15 +494,21 @@ 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
[`--allow-net`]: cli.md#--allow-net
[`--allow-openssl-store`]: cli.md#--allow-openssl-store
[`--allow-wasi`]: cli.md#--allow-wasi
[`--allow-worker`]: cli.md#--allow-worker
[`--env-file-if-exists`]: cli.md#--env-file-if-existsfile
[`--env-file`]: cli.md#--env-filefile
[`--permission-audit`]: cli.md#--permission-audit
[`--permission`]: cli.md#--permission
[`--use-env-proxy`]: cli.md#--use-env-proxy
[`crypto.createPrivateKey()`]: crypto.md#cryptocreateprivatekeykey
[`npx`]: https://docs.npmjs.com/cli/commands/npx
[`permission.drop()`]: process.md#processpermissiondropscope-reference
[`permission.has()`]: process.md#processpermissionhasscope-reference
[`process.loadEnvFile()`]: process.md#processloadenvfilepath
3 changes: 3 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -3163,6 +3163,7 @@ The available scopes are:
* `fs.read` - File System read operations
* `fs.write` - File System write operations
* `child` - Child process spawning operations
* `env` - Environment variables
* `openssl.store` - Loading keys through OpenSSL STORE loaders
* `worker` - Worker thread spawning operation
* `ffi` - Foreign function interface operations
Expand Down Expand Up @@ -3220,6 +3221,8 @@ The available scopes are the same as [`process.permission.has()`][]:
* `fs.read` - File System read operations
* `fs.write` - File System write operations
* `child` - Child process spawning operations
* `env` - Environment variables. Dropping a variable removes it from the
environment
* `openssl.store` - Loading keys through OpenSSL STORE loaders
* `worker` - Worker thread spawning operation
* `net` - Network operations
Expand Down
Loading
Loading