mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
src,lib: add --allow-env permission
Necessarily semver-major. When `--permission` is on, every env var not matched by `--allow-env` is removed at startup. It takes names, prefix patterns (`PREFIX_*`), or `*`, repeatable or comma-sep'd. There are a range of env vars that Node.js itself uses, and a default range that are generally known to be safe in common usage. These are never scrubbed. These include things like `NODE_OPTIONS`, `NODE_EXTRA_CA_CERTS`, `PATH`, `HOME`, etc. `NODE_ENV` is not in the defaults and must be allowed explicitly. Proxy vars (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`) are also not in the defaults since they can carry credentials. When `--use-env-proxy` or `NODE_USE_ENV_PROXY` is set and any of them were removed, a single warning naming them is emitted. Env vars can be dropped at runtime after reading using `permission.drop()`. This is a stronger protection than using `process.env.FOO = undefined` because it will scrub the env var also from the environment block. On Linux, the removed entries are overwritten in the initial environment block. fs reads of any other process's /proc/<pid>/environ, ancestors included, are denied regardless of `--allow-fs-read`. A process's own is readable only with `--allow-env=*`. Symlinks are resolved before the check so paths like /dev/fd/../../<ppid>/environ are caught. The check only canonicalizes paths that statfs() reports are on procfs. On Windows, removal also clears the C runtime's copy of the environ using _wputenv_s. Reading a removed name returns undefined, warns once per name, and publishes to a diagnostics channel. Env file keys are allowed. If the user had reason to pass in an env file the assumption is they meant to allow them. File-source config (node.config.json and NODE_OPTIONS from a .env file) can only narrow the allow list. Embedders must call ScrubProcessEnvironment() themselves on startup. This is left up to the embedder to determine the exact timing but needs to be called before startup actually happens. Child processes are started with `--allow-env=*`. Those either receive the explicit env they were started with or only the env they inherit from the parent. Since the parent process is scrubbed, and the child cannot read any other process's /proc/<pid>/environ, it should never see more than the parent can. Main part of the impl was done by hand. Docs, tests, verification pass, and cleanup nits were automated. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode PR-URL: https://github.com/nodejs/node/pull/66132 Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
1 parent
261c8a196c
commit
f378cfcfbf
47 files changed
+2570
-23
No files matched your search
@@ -191,6 +191,51 @@ 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. `NODE_ENV` is not kept
|
||||
by default, so applications and libraries that read it need
|
||||
`--allow-env=NODE_ENV`. See [Environment variable permissions][] for details.
|
||||
|
||||
### `--allow-ffi`
|
||||
|
||||
<!-- YAML
|
||||
@@ -402,6 +447,11 @@ This flag grants broad authority to configured OpenSSL STORE loaders. A loader
|
||||
may access files, devices, tokens, or the network. Access performed by a loader
|
||||
is not constrained by the `fs.read`, `fs.write`, or `net` permission scopes.
|
||||
|
||||
Loaders and the modules they load are subject to [`--allow-env`][], however.
|
||||
Environment variables they rely on, such as `SOFTHSM2_CONF` for SoftHSM, are
|
||||
removed at startup unless they are granted explicitly with `--allow-env`. See
|
||||
[Environment variable permissions][] for details.
|
||||
|
||||
### `--allow-wasi`
|
||||
|
||||
<!-- YAML
|
||||
@@ -2538,6 +2588,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
|
||||
@@ -4179,6 +4230,7 @@ one is included in the list below.
|
||||
|
||||
* `--allow-addons`
|
||||
* `--allow-child-process`
|
||||
* `--allow-env`
|
||||
* `--allow-ffi`
|
||||
* `--allow-fs-read`
|
||||
* `--allow-fs-vfs`
|
||||
@@ -4826,6 +4878,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
|
||||
@@ -4849,6 +4902,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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+133
-6
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
|
||||
@@ -266,6 +269,109 @@ 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.
|
||||
|
||||
`NODE_ENV` is not kept either. Node.js does not read it, but many applications
|
||||
and libraries do, and treat it being unset as a development environment. Grant
|
||||
access to it explicitly:
|
||||
|
||||
```console
|
||||
$ node --permission --allow-env=NODE_ENV index.js
|
||||
```
|
||||
|
||||
Proxy URLs often contain credentials, so the `HTTP_PROXY`, `HTTPS_PROXY`, and
|
||||
`NO_PROXY` variables, and their lowercase forms, are not kept. Grant access to
|
||||
them explicitly when using [`--use-env-proxy`][]. When `--use-env-proxy` is
|
||||
enabled and any of them were removed at startup, a warning naming them is
|
||||
emitted.
|
||||
|
||||
Native code that Node.js loads on behalf of the application, such as addons,
|
||||
OpenSSL providers and STORE loaders, and the libraries they load in turn, sees
|
||||
the same reduced environment. Only the variables that OpenSSL itself reads are
|
||||
kept, not those read by third-party modules it loads. For example, a PKCS#11
|
||||
provider backed by SoftHSM needs `SOFTHSM2_CONF` to find its token, and fails to
|
||||
initialize without it. Grant access to such variables explicitly:
|
||||
|
||||
```console
|
||||
$ node --permission --allow-openssl-store --allow-env=SOFTHSM2_CONF index.js
|
||||
```
|
||||
|
||||
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. The child can still read its own
|
||||
`/proc/<pid>/environ` on Linux, but not that of any other process, see below.
|
||||
|
||||
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. When the Permission Model is enforced, reading the `/proc/<pid>/environ`
|
||||
file of any other process, including the parent process and its ancestors, is
|
||||
denied regardless of [`--allow-fs-read`][]. Reading the process's own file is
|
||||
only allowed with `--allow-env=*`. Symbolic links are resolved before the
|
||||
check, so paths that reach these files indirectly, such as
|
||||
`/dev/fd/../environ`, are denied as well.
|
||||
|
||||
In addition, the removed variables are overwritten in the initial environment
|
||||
block of the process, so that other processes do not find them in its
|
||||
`/proc/<pid>/environ` either. Variables removed later with
|
||||
[`permission.drop()`][] are overwritten there as well.
|
||||
|
||||
These measures do not change the environment of other processes. 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
|
||||
@@ -297,6 +403,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
|
||||
@@ -348,6 +468,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
|
||||
@@ -410,6 +531,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
|
||||
@@ -417,8 +539,13 @@ Developers relying on --permission to sandbox untrusted code should be aware tha
|
||||
[`--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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+44
@@ -118,6 +118,42 @@ 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 starts without the environment
|
||||
variables it has not been granted access to. At startup, every variable that
|
||||
\fB--allow-env\fR does not match is removed from the process environment. Removed
|
||||
variables are absent from \fBprocess.env\fR, from diagnostic reports, from native
|
||||
code calling \fBgetenv()\fR, and from the environment of child processes and worker
|
||||
threads.
|
||||
The valid values are:
|
||||
.Bl -bullet
|
||||
.It
|
||||
\fB*\fR - Grants access to every environment variable.
|
||||
.It
|
||||
A variable name, for example \fB--allow-env=DATABASE_URL\fR.
|
||||
.It
|
||||
A variable name prefix followed by \fB*\fR, for example \fB--allow-env=APP_*\fR.
|
||||
.El
|
||||
Multiple values can be passed by repeating the flag, or by separating them with
|
||||
commas: \fB--allow-env=PORT,APP_*\fR. Variable names are case-insensitive on
|
||||
Windows.
|
||||
Example:
|
||||
.Bd -literal
|
||||
console.log(process.env.DATABASE_URL);
|
||||
console.log(process.env.AWS_SECRET_ACCESS_KEY);
|
||||
.Ed
|
||||
.Bd -literal
|
||||
$ 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.
|
||||
.Ed
|
||||
The variables that Node.js and its bundled dependencies read, such as
|
||||
\fBNODE_OPTIONS\fR, \fBPATH\fR, \fBHOME\fR, \fBTZ\fR, and \fBSSL_CERT_FILE\fR, are always kept, as
|
||||
are the variables defined in \fB--env-file\fR files. \fBNODE_ENV\fR is not kept
|
||||
by default, so applications and libraries that read it need
|
||||
\fB--allow-env=NODE_ENV\fR. See Environment variable permissions for details.
|
||||
.
|
||||
.It Fl -allow-ffi
|
||||
When using the Permission Model, the process will not be able to use FFI
|
||||
APIs by default. Attempts to use FFI APIs will throw an \fBERR_ACCESS_DENIED\fR
|
||||
@@ -243,6 +279,10 @@ an \fBERR_ACCESS_DENIED\fR unless the user explicitly passes the
|
||||
This flag grants broad authority to configured OpenSSL STORE loaders. A loader
|
||||
may access files, devices, tokens, or the network. Access performed by a loader
|
||||
is not constrained by the \fBfs.read\fR, \fBfs.write\fR, or \fBnet\fR permission scopes.
|
||||
Loaders and the modules they load are subject to \fB--allow-env\fR, however.
|
||||
Environment variables they rely on, such as \fBSOFTHSM2_CONF\fR for SoftHSM, are
|
||||
removed at startup unless they are granted explicitly with \fB--allow-env\fR. See
|
||||
Environment variable permissions for details.
|
||||
.
|
||||
.It Fl -allow-wasi
|
||||
When using the Permission Model, the process will not be capable of creating
|
||||
@@ -1274,6 +1314,8 @@ File System - manageable through
|
||||
.It
|
||||
Network - manageable through \fB--allow-net\fR flag
|
||||
.It
|
||||
Environment variables - manageable through \fB--allow-env\fR flag
|
||||
.It
|
||||
Child Process - manageable through \fB--allow-child-process\fR flag
|
||||
.It
|
||||
Worker Threads - manageable through \fB--allow-worker\fR flag
|
||||
@@ -2124,6 +2166,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
|
||||
|
||||
@@ -570,14 +570,26 @@ function copyPermissionModelFlagsToEnv(env, key, args) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enforcing the permission model removed the variables --allow-env does not
|
||||
// grant access to at startup, so everything left in the environment a child
|
||||
// inherits is accessible. A child can never see more than its parent had.
|
||||
const allowAllEnv = !permission.isAuditMode();
|
||||
|
||||
const flagsToCopy = getPermissionModelFlagsToCopy();
|
||||
for (const arg of process.execArgv) {
|
||||
if (allowAllEnv && arg.startsWith('--allow-env')) {
|
||||
continue;
|
||||
}
|
||||
for (const flag of flagsToCopy) {
|
||||
if (arg.startsWith(flag)) {
|
||||
env[key] = `${env[key] ? env[key] + ' ' + arg : arg}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allowAllEnv) {
|
||||
env[key] = `${env[key] ? env[key] + ' ' : ''}--allow-env=*`;
|
||||
}
|
||||
}
|
||||
|
||||
let emittedDEP0190Already = false;
|
||||
|
||||
@@ -76,6 +76,7 @@ module.exports = ObjectFreeze({
|
||||
'--allow-fs-write',
|
||||
'--allow-addons',
|
||||
'--allow-child-process',
|
||||
'--allow-env',
|
||||
'--allow-net',
|
||||
'--allow-inspector',
|
||||
'--allow-wasi',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayPrototypeFilter,
|
||||
ArrayPrototypeForEach,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypeSplice,
|
||||
Date,
|
||||
DatePrototypeGetDate,
|
||||
@@ -280,6 +282,9 @@ function setupHttpProxy() {
|
||||
if (!getOptionValue('--use-env-proxy')) {
|
||||
return;
|
||||
}
|
||||
// Before reading the variables below, which would warn about each removed
|
||||
// one without saying that it disables the proxy settings.
|
||||
warnAboutRemovedProxyEnvVars();
|
||||
if (!process.env.HTTP_PROXY && !process.env.HTTPS_PROXY &&
|
||||
!process.env.http_proxy && !process.env.https_proxy) {
|
||||
return;
|
||||
@@ -296,6 +301,31 @@ function setupHttpProxy() {
|
||||
// existing libraries that sets the global dispatcher or monkey patches the global agent.
|
||||
}
|
||||
|
||||
// Enforcing the permission model removes the environment variables that
|
||||
// --allow-env does not grant access to at startup. The proxy variables are not
|
||||
// among the variables it keeps by default, because their values may contain
|
||||
// credentials, so --use-env-proxy silently stops applying them unless they
|
||||
// are allowed.
|
||||
function warnAboutRemovedProxyEnvVars() {
|
||||
const permission = require('internal/process/permission');
|
||||
if (!permission.isEnabled() || permission.isAuditMode()) {
|
||||
return;
|
||||
}
|
||||
const { takeRemovedEnvVarWarning } = internalBinding('permission');
|
||||
const removed = ArrayPrototypeFilter([
|
||||
'HTTP_PROXY', 'http_proxy',
|
||||
'HTTPS_PROXY', 'https_proxy',
|
||||
'NO_PROXY', 'no_proxy',
|
||||
], (name) => takeRemovedEnvVarWarning(name));
|
||||
if (removed.length === 0) {
|
||||
return;
|
||||
}
|
||||
process.emitWarning(
|
||||
`--use-env-proxy is enabled, but the permission model removed ${ArrayPrototypeJoin(removed, ', ')} ` +
|
||||
'from the environment at startup, so the proxy settings they define are not applied. ' +
|
||||
'Use --allow-env to grant access to them.');
|
||||
}
|
||||
|
||||
function initializeModuleLoaders(options) {
|
||||
const { shouldSpawnLoaderHookWorker, shouldPreloadModules } = options;
|
||||
// Initialize certain special module.Module properties and the CJS conditions.
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
'src/node_worker.cc',
|
||||
'src/node_zlib.cc',
|
||||
'src/path.cc',
|
||||
'src/permission/env_permission.cc',
|
||||
'src/permission/fs_permission.cc',
|
||||
'src/permission/permission.cc',
|
||||
'src/pipe_wrap.cc',
|
||||
@@ -321,6 +322,7 @@
|
||||
'src/node_worker.h',
|
||||
'src/path.h',
|
||||
'src/permission/boolean_permission.h',
|
||||
'src/permission/env_permission.h',
|
||||
'src/permission/fs_permission.h',
|
||||
'src/permission/permission.h',
|
||||
'src/permission/permission_base.h',
|
||||
|
||||
+15
@@ -8,6 +8,7 @@
|
||||
#include "node_buffer.h"
|
||||
#include "node_context_data.h"
|
||||
#include "node_contextify.h"
|
||||
#include "node_dotenv.h"
|
||||
#include "node_errors.h"
|
||||
#include "node_file_utils.h"
|
||||
#include "node_internals.h"
|
||||
@@ -1136,6 +1137,20 @@ Environment::Environment(IsolateData* isolate_data,
|
||||
permission()->Apply(this, args, permission::PermissionScope::kWASI);
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<std::string> allow_env =
|
||||
permission::ParseEnvAllowList(options_->allow_env);
|
||||
// Variables defined in env files are allowed. The environment scrub
|
||||
// removed any inherited values they had, so only the files' values
|
||||
// are visible.
|
||||
if (options_->has_env_file_string) {
|
||||
for (std::string& key : per_process::dotenv_file.GetKeys()) {
|
||||
allow_env.push_back(std::move(key));
|
||||
}
|
||||
}
|
||||
permission()->Apply(this, allow_env, permission::PermissionScope::kEnv);
|
||||
}
|
||||
|
||||
// Implicit allow entrypoint to kFileSystemRead
|
||||
if (!options_->has_eval_string && !options_->force_repl) {
|
||||
std::string first_argv;
|
||||
|
||||
+155
-6
@@ -46,6 +46,7 @@
|
||||
#include "node_snapshot_builder.h"
|
||||
#include "node_v8_platform-inl.h"
|
||||
#include "node_version.h"
|
||||
#include "permission/env_permission.h"
|
||||
|
||||
#if HAVE_OPENSSL
|
||||
#include "ncrypto.h"
|
||||
@@ -863,6 +864,62 @@ int ProcessGlobalArgs(std::vector<std::string>* args,
|
||||
|
||||
static std::atomic_bool init_called{false};
|
||||
|
||||
// Collects --allow-env per option source. Files (the configuration file, and
|
||||
// NODE_OPTIONS defined in env files) may be controlled by the project being
|
||||
// run rather than by whoever started Node.js, so when the command line or the
|
||||
// NODE_OPTIONS environment variable enable the permission model, --allow-env
|
||||
// values from files can only narrow the access those sources grant.
|
||||
class AllowEnvSources {
|
||||
public:
|
||||
explicit AllowEnvSources(EnvironmentOptions* options) : options_(options) {}
|
||||
|
||||
// Must be called before the options of a source are parsed.
|
||||
void BeginSource() {
|
||||
saved_permission_ = options_->permission;
|
||||
saved_permission_audit_ = options_->permission_audit;
|
||||
options_->permission = false;
|
||||
options_->permission_audit = false;
|
||||
}
|
||||
|
||||
// Must be called after the options of a source are parsed.
|
||||
void EndSource(bool trusted) {
|
||||
if (trusted && (options_->permission || options_->permission_audit)) {
|
||||
trusted_enables_permission_ = true;
|
||||
}
|
||||
options_->permission = options_->permission || saved_permission_;
|
||||
options_->permission_audit =
|
||||
options_->permission_audit || saved_permission_audit_;
|
||||
|
||||
std::vector<std::string>& values = trusted ? trusted_ : from_files_;
|
||||
values.insert(
|
||||
values.end(), options_->allow_env.begin(), options_->allow_env.end());
|
||||
options_->allow_env.clear();
|
||||
}
|
||||
|
||||
// Must be called once every source has been parsed.
|
||||
void Finish() {
|
||||
std::vector<std::string> trusted = permission::ParseEnvAllowList(trusted_);
|
||||
std::vector<std::string> from_files =
|
||||
permission::ParseEnvAllowList(from_files_);
|
||||
if (trusted_enables_permission_ && !from_files.empty()) {
|
||||
options_->allow_env =
|
||||
permission::IntersectEnvAllowLists(trusted, from_files);
|
||||
return;
|
||||
}
|
||||
options_->allow_env = std::move(trusted);
|
||||
options_->allow_env.insert(
|
||||
options_->allow_env.end(), from_files.begin(), from_files.end());
|
||||
}
|
||||
|
||||
private:
|
||||
EnvironmentOptions* options_;
|
||||
std::vector<std::string> trusted_;
|
||||
std::vector<std::string> from_files_;
|
||||
bool trusted_enables_permission_ = false;
|
||||
bool saved_permission_ = false;
|
||||
bool saved_permission_audit_ = false;
|
||||
};
|
||||
|
||||
// TODO(addaleax): Turn this into a wrapper around InitializeOncePerProcess()
|
||||
// (with the corresponding additional flags set), then eventually remove this.
|
||||
static ExitCode InitializeNodeWithArgsInternal(
|
||||
@@ -982,6 +1039,9 @@ static ExitCode InitializeNodeWithArgsInternal(
|
||||
|
||||
node_options = node_options_from_config + node_options_from_dotenv;
|
||||
|
||||
AllowEnvSources allow_env_sources(
|
||||
per_process::cli_options->per_isolate->per_env.get());
|
||||
|
||||
#if !defined(NODE_WITHOUT_NODE_OPTIONS)
|
||||
bool should_parse_node_options =
|
||||
!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv);
|
||||
@@ -995,8 +1055,9 @@ static ExitCode InitializeNodeWithArgsInternal(
|
||||
#endif
|
||||
if (should_parse_node_options) {
|
||||
// NODE_OPTIONS environment variable is preferred over the file one.
|
||||
if (credentials::SafeGetenv("NODE_OPTIONS", &node_options) ||
|
||||
!node_options.empty()) {
|
||||
const bool node_options_from_env =
|
||||
credentials::SafeGetenv("NODE_OPTIONS", &node_options);
|
||||
if (node_options_from_env || !node_options.empty()) {
|
||||
std::vector<std::string> env_argv =
|
||||
ParseNodeOptionsEnvVar(node_options, errors);
|
||||
|
||||
@@ -1005,9 +1066,11 @@ static ExitCode InitializeNodeWithArgsInternal(
|
||||
// [0] is expected to be the program name, fill it in from the real argv.
|
||||
env_argv.insert(env_argv.begin(), argv->at(0));
|
||||
|
||||
allow_env_sources.BeginSource();
|
||||
const ExitCode exit_code = ProcessGlobalArgsInternal(
|
||||
&env_argv, nullptr, errors, kAllowedInEnvvar);
|
||||
if (exit_code != ExitCode::kNoFailure) return exit_code;
|
||||
allow_env_sources.EndSource(/* trusted */ node_options_from_env);
|
||||
}
|
||||
} else {
|
||||
std::string node_repl_external_env = {};
|
||||
@@ -1030,15 +1093,21 @@ static ExitCode InitializeNodeWithArgsInternal(
|
||||
// [0] is expected to be the program name, fill it in from the real argv.
|
||||
extra_argv.insert(extra_argv.begin(), argv->at(0));
|
||||
// Parse the extra argv coming from the config file
|
||||
allow_env_sources.BeginSource();
|
||||
ExitCode exit_code = ProcessGlobalArgsInternal(
|
||||
&extra_argv, nullptr, errors, kDisallowedInEnvvar);
|
||||
if (exit_code != ExitCode::kNoFailure) return exit_code;
|
||||
allow_env_sources.EndSource(/* trusted */ false);
|
||||
// Parse options coming from the command line.
|
||||
allow_env_sources.BeginSource();
|
||||
exit_code =
|
||||
ProcessGlobalArgsInternal(argv, exec_argv, errors, kDisallowedInEnvvar);
|
||||
if (exit_code != ExitCode::kNoFailure) return exit_code;
|
||||
allow_env_sources.EndSource(/* trusted */ true);
|
||||
}
|
||||
|
||||
allow_env_sources.Finish();
|
||||
|
||||
// Every option source has now been parsed, so cross-source option
|
||||
// constraints can finally be validated.
|
||||
CheckGlobalBenchOptions(errors);
|
||||
@@ -1166,10 +1235,17 @@ bool CanEnableWebAssemblyTrapHandler() {
|
||||
}
|
||||
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
|
||||
|
||||
// Whether InitializeOncePerProcessInternal() scrubs the process environment
|
||||
// when the permission model restricts access to it. Only node::Start() does
|
||||
// this. Embedders own their process environment, and scrub it themselves.
|
||||
enum class EnvironmentScrubMode { kNever, kIfRestricted };
|
||||
|
||||
static std::shared_ptr<InitializationResultImpl>
|
||||
InitializeOncePerProcessInternal(const std::vector<std::string>& args,
|
||||
ProcessInitializationFlags::Flags flags =
|
||||
ProcessInitializationFlags::kNoFlags) {
|
||||
InitializeOncePerProcessInternal(
|
||||
const std::vector<std::string>& args,
|
||||
ProcessInitializationFlags::Flags flags =
|
||||
ProcessInitializationFlags::kNoFlags,
|
||||
EnvironmentScrubMode scrub_mode = EnvironmentScrubMode::kNever) {
|
||||
auto result = std::make_shared<InitializationResultImpl>();
|
||||
result->args_ = args;
|
||||
|
||||
@@ -1358,6 +1434,54 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
|
||||
#endif // HAVE_OPENSSL
|
||||
}
|
||||
|
||||
{
|
||||
const auto& env_options = per_process::cli_options->per_isolate->per_env;
|
||||
const std::vector<std::string> allow =
|
||||
permission::ParseEnvAllowList(env_options->allow_env);
|
||||
const bool allow_all =
|
||||
std::find(allow.begin(), allow.end(), "*") != allow.end();
|
||||
if (env_options->permission_audit && !allow_all) {
|
||||
// In audit mode nothing is removed, but accesses to the variables that
|
||||
// enforcing the permission model would remove are published.
|
||||
permission::RecordAuditedEnvironmentVariables(allow);
|
||||
} else if (env_options->permission && !allow_all) {
|
||||
if (scrub_mode == EnvironmentScrubMode::kIfRestricted) {
|
||||
// When the permission model is enforced, remove every environment
|
||||
// variable that --allow-env does not grant access to. Every
|
||||
// per-process consumer of the environment has read it by now, and the
|
||||
// process is still single-threaded: the platform worker threads start
|
||||
// below.
|
||||
permission::ScrubProcessEnvironment({.allow = allow});
|
||||
} else {
|
||||
// Embedders own the process environment, and must remove these
|
||||
// variables themselves, with ScrubProcessEnvironment().
|
||||
const std::vector<std::string> denied =
|
||||
permission::FindDeniedEnvironmentVariables(allow);
|
||||
if (!denied.empty()) {
|
||||
constexpr size_t kMaxListedNames = 5;
|
||||
std::string names;
|
||||
for (size_t i = 0; i < denied.size() && i < kMaxListedNames; i++) {
|
||||
if (i > 0) names += ", ";
|
||||
names += denied[i];
|
||||
}
|
||||
if (denied.size() > kMaxListedNames) {
|
||||
names += ", and " +
|
||||
std::to_string(denied.size() - kMaxListedNames) + " more";
|
||||
}
|
||||
result->errors_.push_back(
|
||||
"The process environment contains variables that --allow-env "
|
||||
"does not grant access to (" +
|
||||
names +
|
||||
"). Remove them with node::ScrubProcessEnvironment() before "
|
||||
"calling node::InitializeOncePerProcess().");
|
||||
result->exit_code_ = ExitCode::kInvalidCommandLineArgument;
|
||||
result->early_return_ = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(flags & ProcessInitializationFlags::kNoInitializeNodeV8Platform)) {
|
||||
uv_thread_setname("node-MainThread");
|
||||
per_process::v8_platform.Initialize(
|
||||
@@ -1431,6 +1555,29 @@ std::shared_ptr<InitializationResult> InitializeOncePerProcess(
|
||||
return InitializeOncePerProcessInternal(args, flags);
|
||||
}
|
||||
|
||||
v8::Maybe<std::vector<std::string>> ScrubProcessEnvironment(
|
||||
const ProcessEnvironmentScrubOptions& options) {
|
||||
if (per_process::v8_initialized) {
|
||||
return v8::Nothing<std::vector<std::string>>();
|
||||
}
|
||||
for (const std::string& pattern : options.allow) {
|
||||
if (!permission::IsValidEnvAllowPattern(pattern)) {
|
||||
return v8::Nothing<std::vector<std::string>>();
|
||||
}
|
||||
}
|
||||
return v8::Just(permission::ScrubProcessEnvironment({
|
||||
.allow = options.allow,
|
||||
.keep_runtime_defaults = options.keep_runtime_defaults,
|
||||
.wipe_initial_block = options.wipe_initial_block,
|
||||
}));
|
||||
}
|
||||
|
||||
std::vector<std::string> GetRuntimeEnvironmentDefaults() {
|
||||
const std::span<const std::string_view> defaults =
|
||||
permission::GetRuntimeEnvironmentDefaults();
|
||||
return std::vector<std::string>(defaults.begin(), defaults.end());
|
||||
}
|
||||
|
||||
void TearDownOncePerProcess() {
|
||||
const uint32_t flags = init_process_flags.load();
|
||||
ResetStdio();
|
||||
@@ -1662,7 +1809,9 @@ static ExitCode StartInternal(int argc, char** argv) {
|
||||
|
||||
std::shared_ptr<InitializationResultImpl> result =
|
||||
InitializeOncePerProcessInternal(
|
||||
std::vector<std::string>(argv, argv + argc));
|
||||
std::vector<std::string>(argv, argv + argc),
|
||||
ProcessInitializationFlags::kNoFlags,
|
||||
EnvironmentScrubMode::kIfRestricted);
|
||||
for (const std::string& error : result->errors()) {
|
||||
FPrintF(stderr, "%s: %s\n", result->args().at(0), error);
|
||||
}
|
||||
|
||||
+36
@@ -324,6 +324,42 @@ inline std::shared_ptr<InitializationResult> InitializeOncePerProcess(
|
||||
args, static_cast<ProcessInitializationFlags::Flags>(flags_accum));
|
||||
}
|
||||
|
||||
struct ProcessEnvironmentScrubOptions {
|
||||
// The environment variables to keep. Each entry is `*`, a variable name, or
|
||||
// a variable name prefix followed by `*`. Names are case-insensitive on
|
||||
// Windows.
|
||||
std::vector<std::string> allow;
|
||||
// Whether to also keep the variables that Node.js and its bundled
|
||||
// dependencies read after startup, see GetRuntimeEnvironmentDefaults().
|
||||
bool keep_runtime_defaults = true;
|
||||
// Whether to overwrite the removed variables in the environment block the
|
||||
// process was started with, which /proc/<pid>/environ exposes. Only
|
||||
// implemented on Linux.
|
||||
bool wipe_initial_block = true;
|
||||
};
|
||||
|
||||
// Removes every variable that `options` does not keep from the process
|
||||
// environment, and returns the names of the removed variables.
|
||||
//
|
||||
// node::Start() does this automatically when the permission model restricts
|
||||
// access to environment variables. When `args` passed to
|
||||
// InitializeOncePerProcess() enable the permission model without
|
||||
// `--allow-env=*`, the embedder must remove the variables that `--allow-env`
|
||||
// does not grant access to first: InitializeOncePerProcess() fails if the
|
||||
// process environment contains any of them.
|
||||
//
|
||||
// This 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
|
||||
// InitializeOncePerProcess(). Returns Nothing() if `options.allow` contains an
|
||||
// invalid entry, or if InitializeOncePerProcess() has already completed.
|
||||
NODE_EXTERN v8::Maybe<std::vector<std::string>> ScrubProcessEnvironment(
|
||||
const ProcessEnvironmentScrubOptions& options);
|
||||
|
||||
// Returns the names, and name prefixes followed by `*`, of the environment
|
||||
// variables that Node.js and its bundled dependencies read after startup.
|
||||
NODE_EXTERN std::vector<std::string> GetRuntimeEnvironmentDefaults();
|
||||
|
||||
enum OptionEnvvarSettings {
|
||||
// Allow the options to be set via the environment variable, like
|
||||
// `NODE_OPTIONS`.
|
||||
|
||||
@@ -86,6 +86,15 @@ Maybe<void> Dotenv::SetEnvironment(node::Environment* env) {
|
||||
return JustVoid();
|
||||
}
|
||||
|
||||
std::vector<std::string> Dotenv::GetKeys() const {
|
||||
std::vector<std::string> keys;
|
||||
keys.reserve(store_.size());
|
||||
for (const auto& entry : store_) {
|
||||
keys.push_back(entry.first);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
MaybeLocal<Object> Dotenv::ToObject(Environment* env) const {
|
||||
EscapableHandleScope scope(env->isolate());
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ class Dotenv {
|
||||
void AssignNodeOptionsIfAvailable(std::string* node_options) const;
|
||||
v8::Maybe<void> SetEnvironment(Environment* env);
|
||||
v8::MaybeLocal<v8::Object> ToObject(Environment* env) const;
|
||||
// The names of the variables parsed from the env files.
|
||||
std::vector<std::string> GetKeys() const;
|
||||
|
||||
static std::vector<env_file_data> GetDataFromArgs(
|
||||
const std::vector<std::string>& args);
|
||||
@@ -38,6 +40,11 @@ class Dotenv {
|
||||
std::map<std::string, std::string> store_;
|
||||
};
|
||||
|
||||
namespace per_process {
|
||||
// The env files passed with --env-file and --env-file-if-exists.
|
||||
extern Dotenv dotenv_file;
|
||||
} // namespace per_process
|
||||
|
||||
} // namespace node
|
||||
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "node_external_reference.h"
|
||||
#include "node_i18n.h"
|
||||
#include "node_process-inl.h"
|
||||
#include "permission/env_permission.h"
|
||||
#include "permission/permission.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <time.h> // tzset(), _tzset()
|
||||
@@ -429,6 +431,40 @@ void TraceEnvVar(Environment* env,
|
||||
}
|
||||
}
|
||||
|
||||
// Called when process.env does not have `property`. If the permission model
|
||||
// removed the variable at startup, publishes the denial and warns once, so
|
||||
// that the variable does not just silently read as undefined.
|
||||
static Maybe<void> ReportRemovedEnvVar(Environment* env,
|
||||
Local<String> property) {
|
||||
if (!permission::IsProcessEnvironmentScrubbed()) return JustVoid();
|
||||
Utf8Value key(env->isolate(), property);
|
||||
if (!permission::WasRemovedByEnvironmentScrub(key.ToStringView())) {
|
||||
return JustVoid();
|
||||
}
|
||||
env->permission()->PublishDenied(
|
||||
env, permission::PermissionScope::kEnv, key.ToStringView());
|
||||
if (permission::ShouldWarnAboutRemovedEnvVar(key.ToStringView()) &&
|
||||
ProcessEmitWarning(env,
|
||||
"The permission model removed the environment "
|
||||
"variable \"%s\" at startup. Use --allow-env to "
|
||||
"manage permissions.",
|
||||
*key)
|
||||
.IsNothing()) {
|
||||
return Nothing<void>();
|
||||
}
|
||||
return JustVoid();
|
||||
}
|
||||
|
||||
// In audit mode nothing is removed at startup. Publishes accesses to the
|
||||
// variables that enforcing the permission model would have removed instead.
|
||||
static void AuditEnvVar(Environment* env, Local<String> property) {
|
||||
Utf8Value key(env->isolate(), property);
|
||||
if (!permission::WasDeniedAtStartup(key.ToStringView())) return;
|
||||
// is_granted() publishes the denial.
|
||||
env->permission()->is_granted(
|
||||
env, permission::PermissionScope::kEnv, key.ToStringView());
|
||||
}
|
||||
|
||||
static Intercepted EnvGetter(Local<Name> property,
|
||||
const PropertyCallbackInfo<Value>& info) {
|
||||
Environment* env = Environment::GetCurrent(info);
|
||||
@@ -445,8 +481,15 @@ static Intercepted EnvGetter(Local<Name> property,
|
||||
|
||||
Local<Value> ret;
|
||||
if (!value_string.ToLocal(&ret)) {
|
||||
if (env->permission()->enabled() &&
|
||||
ReportRemovedEnvVar(env, property.As<String>()).IsNothing()) {
|
||||
return Intercepted::kYes;
|
||||
}
|
||||
return Intercepted::kNo;
|
||||
}
|
||||
if (env->permission()->warning_only()) {
|
||||
AuditEnvVar(env, property.As<String>());
|
||||
}
|
||||
info.GetReturnValue().Set(ret);
|
||||
return Intercepted::kYes;
|
||||
}
|
||||
@@ -496,10 +539,17 @@ static Intercepted EnvQuery(Local<Name> property,
|
||||
bool has_env = (rc != -1);
|
||||
TraceEnvVar(env, "query", property.As<String>());
|
||||
if (has_env) {
|
||||
if (env->permission()->warning_only()) {
|
||||
AuditEnvVar(env, property.As<String>());
|
||||
}
|
||||
// Return attributes for the property.
|
||||
info.GetReturnValue().Set(v8::None);
|
||||
return Intercepted::kYes;
|
||||
}
|
||||
if (env->permission()->enabled() &&
|
||||
ReportRemovedEnvVar(env, property.As<String>()).IsNothing()) {
|
||||
return Intercepted::kYes;
|
||||
}
|
||||
}
|
||||
return Intercepted::kNo;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "node_external_reference.h"
|
||||
#include "node_internals.h"
|
||||
#include "node_sea.h"
|
||||
#include "permission/env_permission.h"
|
||||
#include "uv.h"
|
||||
#if HAVE_OPENSSL
|
||||
#include "ncrypto.h" // Defines OPENSSL_VERSION_PREREQ for BoringSSL.
|
||||
@@ -315,6 +316,14 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
|
||||
errors->push_back("either --check or --eval can be used, not both");
|
||||
}
|
||||
|
||||
for (const std::string& pattern : permission::ParseEnvAllowList(allow_env)) {
|
||||
if (!permission::IsValidEnvAllowPattern(pattern)) {
|
||||
errors->push_back("--allow-env must be '*', a variable name, or a "
|
||||
"variable name prefix followed by '*'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!unhandled_rejections.empty() &&
|
||||
unhandled_rejections != "warn-with-error-code" &&
|
||||
unhandled_rejections != "throw" &&
|
||||
@@ -859,6 +868,12 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
|
||||
BOOL_FIELD(permission_audit),
|
||||
kAllowedInEnvvar,
|
||||
false);
|
||||
AddOption("--allow-env",
|
||||
"allow access to environment variables when any permissions are "
|
||||
"set",
|
||||
&EnvironmentOptions::allow_env,
|
||||
kAllowedInEnvvar,
|
||||
OptionNamespaces::kPermissionNamespace);
|
||||
AddOption("--allow-fs-read",
|
||||
"allow permissions to read the filesystem",
|
||||
&EnvironmentOptions::allow_fs_read,
|
||||
|
||||
@@ -161,6 +161,7 @@ class EnvironmentOptions : public Options {
|
||||
#endif // HAVE_INSPECTOR
|
||||
|
||||
std::vector<std::string> conditions;
|
||||
std::vector<std::string> allow_env;
|
||||
std::vector<std::string> allow_fs_read;
|
||||
std::vector<std::string> allow_fs_write;
|
||||
std::vector<std::string> disable_warnings;
|
||||
|
||||
@@ -0,0 +1,618 @@
|
||||
#include "permission/env_permission.h"
|
||||
|
||||
#include "env-inl.h"
|
||||
#include "node_internals.h"
|
||||
#include "node_mutex.h"
|
||||
#include "util-inl.h"
|
||||
#include "uv.h"
|
||||
#include "v8.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <linux/magic.h> // PROC_SUPER_MAGIC
|
||||
#include <sys/vfs.h> // statfs()
|
||||
|
||||
extern char** environ;
|
||||
#endif
|
||||
|
||||
namespace node {
|
||||
|
||||
using v8::Array;
|
||||
using v8::Context;
|
||||
using v8::HandleScope;
|
||||
using v8::Isolate;
|
||||
using v8::Local;
|
||||
using v8::LocalVector;
|
||||
using v8::String;
|
||||
using v8::Value;
|
||||
|
||||
namespace permission {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kRuntimeEnvironmentDefaults[] = {
|
||||
// Read by Node.js itself after startup.
|
||||
"NODE_BENCH_CONTEXT",
|
||||
"NODE_BENCH_FILE_RUN_ID",
|
||||
"NODE_BENCH_RUN_ID",
|
||||
"NODE_CHANNEL_FD",
|
||||
"NODE_CHANNEL_SERIALIZATION_MODE",
|
||||
"NODE_CLUSTER_SCHED_POLICY",
|
||||
"NODE_COMPILE_CACHE",
|
||||
"NODE_COMPILE_CACHE_PORTABLE",
|
||||
"NODE_COMPILE_CACHE_READONLY",
|
||||
"NODE_DEBUG",
|
||||
"NODE_DEBUG_NATIVE",
|
||||
"NODE_DISABLE_COLORS",
|
||||
"NODE_DISABLE_COMPILE_CACHE",
|
||||
"NODE_EXTRA_CA_CERTS",
|
||||
"NODE_ICU_DATA",
|
||||
"NODE_INSPECT_RESUME_ON_START",
|
||||
"NODE_NO_WARNINGS",
|
||||
"NODE_OPTIONS",
|
||||
"NODE_PATH",
|
||||
"NODE_PENDING_DEPRECATION",
|
||||
"NODE_PENDING_PIPE_INSTANCES",
|
||||
"NODE_PRESERVE_SYMLINKS",
|
||||
"NODE_REDIRECT_WARNINGS",
|
||||
"NODE_REPL_EXTERNAL_MODULE",
|
||||
"NODE_REPL_HISTORY",
|
||||
"NODE_TEST_CONTEXT",
|
||||
"NODE_TEST_WORKER_ID",
|
||||
"NODE_TLS_REJECT_UNAUTHORIZED",
|
||||
"NODE_UNIQUE_ID",
|
||||
"NODE_USE_ENV_PROXY",
|
||||
"NODE_USE_SYSTEM_CA",
|
||||
"NODE_V8_COVERAGE",
|
||||
"WATCH_REPORT_DEPENDENCIES",
|
||||
// Terminal and CI detection, see lib/internal/tty.js.
|
||||
"APPVEYOR",
|
||||
"BUILDKITE",
|
||||
"CI",
|
||||
"CI_NAME",
|
||||
"CIRCLECI",
|
||||
"COLORTERM",
|
||||
"DRONE",
|
||||
"FORCE_COLOR",
|
||||
"GITEA_ACTIONS",
|
||||
"GITHUB_ACTIONS",
|
||||
"GITLAB_CI",
|
||||
"NO_COLOR",
|
||||
"TEAMCITY_VERSION",
|
||||
"TERM",
|
||||
"TERM_PROGRAM",
|
||||
"TERM_PROGRAM_VERSION",
|
||||
"TMUX",
|
||||
"TRAVIS",
|
||||
// libuv.
|
||||
"HOME",
|
||||
"PATH",
|
||||
"TEMP",
|
||||
"TMP",
|
||||
"TMPDIR",
|
||||
"USERPROFILE",
|
||||
"UV_THREADPOOL_SIZE",
|
||||
"UV_USE_IO_URING",
|
||||
// Copied into child process environments by libuv on Windows, and read
|
||||
// by lib/child_process.js.
|
||||
"COMSPEC",
|
||||
"HOMEDRIVE",
|
||||
"HOMEPATH",
|
||||
"LOGONSERVER",
|
||||
"SYSTEMDRIVE",
|
||||
"SYSTEMROOT",
|
||||
"USERDOMAIN",
|
||||
"USERNAME",
|
||||
"WINDIR",
|
||||
// c-ares and the system resolver.
|
||||
"HOSTALIASES",
|
||||
"LOCALDOMAIN",
|
||||
"RES_OPTIONS",
|
||||
// ICU, the time zone and the locale.
|
||||
"ICU_DATA",
|
||||
"LANG",
|
||||
"LANGUAGE",
|
||||
"LC_*",
|
||||
"TZ",
|
||||
// OpenSSL.
|
||||
"OPENSSL_CONF",
|
||||
"OPENSSL_CONF_INCLUDE",
|
||||
"OPENSSL_ENGINES",
|
||||
"OPENSSL_MODULES",
|
||||
"SSL_CERT_DIR",
|
||||
"SSL_CERT_FILE",
|
||||
// The dynamic loader, for addons and FFI.
|
||||
"DYLD_FALLBACK_LIBRARY_PATH",
|
||||
"DYLD_LIBRARY_PATH",
|
||||
"LD_LIBRARY_PATH",
|
||||
"LIBPATH",
|
||||
};
|
||||
|
||||
struct EnvScrubState {
|
||||
Mutex mutex;
|
||||
// Whether ScrubProcessEnvironment() removed the variables in `denied`.
|
||||
std::atomic<bool> scrubbed{false};
|
||||
// Whether RecordAuditedEnvironmentVariables() recorded them instead.
|
||||
std::atomic<bool> audited{false};
|
||||
std::unordered_set<std::string> denied;
|
||||
std::unordered_set<std::string> warned;
|
||||
};
|
||||
|
||||
EnvScrubState& GetScrubState() {
|
||||
// Intentionally leaked, so that it can be used during process teardown.
|
||||
static EnvScrubState* state = new EnvScrubState();
|
||||
return *state;
|
||||
}
|
||||
|
||||
// The key under which a name is stored in the sets above.
|
||||
std::string NormalizeEnvName(std::string_view name) {
|
||||
#ifdef _WIN32
|
||||
return ToUpper(std::string(name));
|
||||
#else
|
||||
return std::string(name);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MatchesAnyPattern(std::span<const std::string> patterns,
|
||||
std::string_view name) {
|
||||
return std::any_of(
|
||||
patterns.begin(), patterns.end(), [&](const std::string& pattern) {
|
||||
return EnvNameMatchesPattern(pattern, name);
|
||||
});
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// libuv modifies the environment block of the process, but the C runtime keeps
|
||||
// its own copy of the environment, which getenv() reads. Removes a variable
|
||||
// from that copy.
|
||||
void UnsetCrtEnvironmentVariable(const std::string& name) {
|
||||
_wputenv_s(ConvertUTF8ToWideString(name).c_str(), L"");
|
||||
}
|
||||
#endif // _WIN32
|
||||
|
||||
// The entries of the environment block the process was started with, as
|
||||
// [pointer, length) pairs.
|
||||
using InitialEnvEntries = std::vector<std::pair<char*, size_t>>;
|
||||
|
||||
#if defined(__linux__)
|
||||
// Reads the [start, end) address range of the environment block the process
|
||||
// was started with from fields 50 and 51 of /proc/self/stat (Linux 3.5+).
|
||||
bool GetInitialEnvironmentBlock(uintptr_t* start, uintptr_t* end) {
|
||||
FILE* fp = fopen("/proc/self/stat", "re");
|
||||
if (fp == nullptr) return false;
|
||||
char buf[4096];
|
||||
size_t length = fread(buf, 1, sizeof(buf) - 1, fp);
|
||||
fclose(fp);
|
||||
buf[length] = '\0';
|
||||
|
||||
// The command name in field 2 may contain spaces and parentheses, so start
|
||||
// after the last ')', where field 3 begins.
|
||||
char* p = strrchr(buf, ')');
|
||||
if (p == nullptr) return false;
|
||||
p++;
|
||||
|
||||
constexpr int kEnvStartField = 50;
|
||||
constexpr int kEnvEndField = 51;
|
||||
for (int field = 3; field <= kEnvEndField; field++) {
|
||||
char* next;
|
||||
uint64_t value = strtoull(p, &next, 10);
|
||||
if (next == p) {
|
||||
// A non-numeric field, such as the state in field 3.
|
||||
while (*p == ' ') p++;
|
||||
while (*p != ' ' && *p != '\0') p++;
|
||||
if (*p == '\0') return false;
|
||||
} else {
|
||||
p = next;
|
||||
}
|
||||
if (field == kEnvStartField) *start = static_cast<uintptr_t>(value);
|
||||
if (field == kEnvEndField) *end = static_cast<uintptr_t>(value);
|
||||
}
|
||||
return *start != 0 && *start < *end;
|
||||
}
|
||||
|
||||
// Returns the entries for `names` that still point into the initial
|
||||
// environment block, which unsetenv() leaves untouched.
|
||||
InitialEnvEntries FindInitialEnvironmentEntries(
|
||||
const std::vector<std::string>& names) {
|
||||
InitialEnvEntries entries;
|
||||
uintptr_t start = 0;
|
||||
uintptr_t end = 0;
|
||||
if (!GetInitialEnvironmentBlock(&start, &end)) return entries;
|
||||
|
||||
for (char** e = environ; e != nullptr && *e != nullptr; e++) {
|
||||
char* entry = *e;
|
||||
const uintptr_t address = reinterpret_cast<uintptr_t>(entry);
|
||||
if (address < start || address >= end) continue;
|
||||
const char* equals = strchr(entry, '=');
|
||||
if (equals == nullptr) continue;
|
||||
const std::string_view name(entry, equals - entry);
|
||||
if (std::find(names.begin(), names.end(), name) == names.end()) continue;
|
||||
size_t length = strlen(entry);
|
||||
if (address + length > end) length = end - address;
|
||||
entries.emplace_back(entry, length);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
#else
|
||||
InitialEnvEntries FindInitialEnvironmentEntries(
|
||||
const std::vector<std::string>&) {
|
||||
return {};
|
||||
}
|
||||
#endif // defined(__linux__)
|
||||
|
||||
// Overwrites the entries returned by FindInitialEnvironmentEntries(). Call
|
||||
// this only once the variables no longer appear in `environ`, so that
|
||||
// /proc/<pid>/environ stops exposing their values.
|
||||
void WipeInitialEnvironmentEntries(const InitialEnvEntries& entries) {
|
||||
for (const auto& [entry, length] : entries) {
|
||||
memset(entry, 0, length);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::string> ParseEnvAllowList(
|
||||
std::span<const std::string> values) {
|
||||
std::vector<std::string> patterns;
|
||||
for (const std::string& value : values) {
|
||||
size_t begin = 0;
|
||||
while (begin <= value.size()) {
|
||||
size_t end = value.find(',', begin);
|
||||
if (end == std::string::npos) end = value.size();
|
||||
if (end > begin) patterns.emplace_back(value.substr(begin, end - begin));
|
||||
begin = end + 1;
|
||||
}
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
|
||||
bool IsValidEnvAllowPattern(std::string_view pattern) {
|
||||
const size_t wildcard = pattern.find('*');
|
||||
return !pattern.empty() && pattern.find('=') == std::string_view::npos &&
|
||||
(wildcard == std::string_view::npos || wildcard == pattern.size() - 1);
|
||||
}
|
||||
|
||||
bool EnvNameMatchesPattern(std::string_view pattern, std::string_view name) {
|
||||
if (pattern == "*") return true;
|
||||
const bool is_prefix = !pattern.empty() && pattern.back() == '*';
|
||||
if (is_prefix) pattern.remove_suffix(1);
|
||||
if (is_prefix ? name.size() < pattern.size()
|
||||
: name.size() != pattern.size()) {
|
||||
return false;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
for (size_t i = 0; i < pattern.size(); i++) {
|
||||
if (ToUpper(pattern[i]) != ToUpper(name[i])) return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return name.compare(0, pattern.size(), pattern) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Whether every name `specific` matches is also matched by `general`.
|
||||
static bool EnvPatternCovers(std::string_view general,
|
||||
std::string_view specific) {
|
||||
if (general == "*") return true;
|
||||
if (specific.empty() || specific == "*") return false;
|
||||
if (specific.back() != '*') return EnvNameMatchesPattern(general, specific);
|
||||
// Two prefixes: `P*` only matches names `G*` matches if P starts with G.
|
||||
if (general.empty() || general.back() != '*') return false;
|
||||
specific.remove_suffix(1);
|
||||
return EnvNameMatchesPattern(general, specific);
|
||||
}
|
||||
|
||||
std::vector<std::string> IntersectEnvAllowLists(
|
||||
std::span<const std::string> a, std::span<const std::string> b) {
|
||||
std::vector<std::string> result;
|
||||
for (const std::string& x : a) {
|
||||
for (const std::string& y : b) {
|
||||
if (EnvPatternCovers(x, y)) {
|
||||
result.push_back(y);
|
||||
} else if (EnvPatternCovers(y, x)) {
|
||||
result.push_back(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::span<const std::string_view> GetRuntimeEnvironmentDefaults() {
|
||||
return kRuntimeEnvironmentDefaults;
|
||||
}
|
||||
|
||||
bool IsRuntimeEnvironmentDefault(std::string_view name) {
|
||||
return std::any_of(std::begin(kRuntimeEnvironmentDefaults),
|
||||
std::end(kRuntimeEnvironmentDefaults),
|
||||
[&](std::string_view pattern) {
|
||||
return EnvNameMatchesPattern(pattern, name);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns the names of the variables in the process environment that neither
|
||||
// `allow` nor, if `keep_runtime_defaults` is set, the runtime defaults match.
|
||||
// The caller must hold per_process::env_var_mutex.
|
||||
static std::vector<std::string> FindUnmatchedEnvironmentVariables(
|
||||
std::span<const std::string> allow, bool keep_runtime_defaults) {
|
||||
uv_env_item_t* items = nullptr;
|
||||
int count = 0;
|
||||
// Failing to enumerate the environment must not leave it unscrubbed.
|
||||
CHECK_EQ(uv_os_environ(&items, &count), 0);
|
||||
auto cleanup = OnScopeLeave([&]() { uv_os_free_environ(items, count); });
|
||||
|
||||
std::vector<std::string> names;
|
||||
for (int i = 0; i < count; i++) {
|
||||
const std::string_view name(items[i].name);
|
||||
if (name.empty()) continue;
|
||||
#ifdef _WIN32
|
||||
// Hidden variables, such as the per-drive working directories ("=C:").
|
||||
if (name[0] == '=') continue;
|
||||
#endif
|
||||
if (keep_runtime_defaults && IsRuntimeEnvironmentDefault(name)) continue;
|
||||
if (MatchesAnyPattern(allow, name)) continue;
|
||||
names.emplace_back(name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
std::vector<std::string> FindDeniedEnvironmentVariables(
|
||||
std::span<const std::string> allow) {
|
||||
Mutex::ScopedLock env_lock(per_process::env_var_mutex);
|
||||
return FindUnmatchedEnvironmentVariables(allow, true);
|
||||
}
|
||||
|
||||
std::vector<std::string> ScrubProcessEnvironment(
|
||||
const EnvScrubOptions& options) {
|
||||
EnvScrubState& state = GetScrubState();
|
||||
Mutex::ScopedLock state_lock(state.mutex);
|
||||
Mutex::ScopedLock env_lock(per_process::env_var_mutex);
|
||||
|
||||
std::vector<std::string> removed = FindUnmatchedEnvironmentVariables(
|
||||
options.allow, options.keep_runtime_defaults);
|
||||
|
||||
// Locate the entries before unsetting the variables, which removes them
|
||||
// from `environ` and so puts them out of reach.
|
||||
InitialEnvEntries initial_entries;
|
||||
if (options.wipe_initial_block) {
|
||||
initial_entries = FindInitialEnvironmentEntries(removed);
|
||||
}
|
||||
|
||||
for (const std::string& name : removed) {
|
||||
uv_os_unsetenv(name.c_str());
|
||||
#ifdef _WIN32
|
||||
UnsetCrtEnvironmentVariable(name);
|
||||
#endif
|
||||
state.denied.insert(NormalizeEnvName(name));
|
||||
}
|
||||
|
||||
// Now that environ no longer refers to them, overwrite the removed entries
|
||||
// so that /proc/<pid>/environ does not expose them either.
|
||||
WipeInitialEnvironmentEntries(initial_entries);
|
||||
|
||||
state.scrubbed.store(true);
|
||||
return removed;
|
||||
}
|
||||
|
||||
void RecordAuditedEnvironmentVariables(std::span<const std::string> allow) {
|
||||
EnvScrubState& state = GetScrubState();
|
||||
Mutex::ScopedLock state_lock(state.mutex);
|
||||
Mutex::ScopedLock env_lock(per_process::env_var_mutex);
|
||||
for (const std::string& name :
|
||||
FindUnmatchedEnvironmentVariables(allow, true)) {
|
||||
state.denied.insert(NormalizeEnvName(name));
|
||||
}
|
||||
state.audited.store(true);
|
||||
}
|
||||
|
||||
bool IsProcessEnvironmentScrubbed() {
|
||||
return GetScrubState().scrubbed.load();
|
||||
}
|
||||
|
||||
bool WasRemovedByEnvironmentScrub(std::string_view name) {
|
||||
EnvScrubState& state = GetScrubState();
|
||||
if (!state.scrubbed.load()) return false;
|
||||
Mutex::ScopedLock lock(state.mutex);
|
||||
return state.denied.contains(NormalizeEnvName(name));
|
||||
}
|
||||
|
||||
bool WasDeniedAtStartup(std::string_view name) {
|
||||
EnvScrubState& state = GetScrubState();
|
||||
if (!state.scrubbed.load() && !state.audited.load()) return false;
|
||||
Mutex::ScopedLock lock(state.mutex);
|
||||
return state.denied.contains(NormalizeEnvName(name));
|
||||
}
|
||||
|
||||
bool ShouldWarnAboutRemovedEnvVar(std::string_view name) {
|
||||
EnvScrubState& state = GetScrubState();
|
||||
Mutex::ScopedLock lock(state.mutex);
|
||||
return state.warned.insert(NormalizeEnvName(name)).second;
|
||||
}
|
||||
|
||||
void EnvPermission::Apply(Environment* env,
|
||||
std::span<const std::string> allow,
|
||||
PermissionScope scope) {
|
||||
RwLock::ScopedWriteLock lock(lock_);
|
||||
patterns_.assign(allow.begin(), allow.end());
|
||||
if (std::find(patterns_.begin(), patterns_.end(), "*") != patterns_.end()) {
|
||||
granted_all_.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
void EnvPermission::Drop(Environment* env,
|
||||
PermissionScope scope,
|
||||
std::string_view param) {
|
||||
{
|
||||
RwLock::ScopedWriteLock lock(lock_);
|
||||
granted_all_.store(false, std::memory_order_release);
|
||||
if (param.empty()) {
|
||||
dropped_all_ = true;
|
||||
patterns_.clear();
|
||||
dropped_.clear();
|
||||
} else {
|
||||
dropped_.insert(NormalizeEnvName(param));
|
||||
}
|
||||
}
|
||||
// Not under lock_: this calls into the KVStore, which takes
|
||||
// per_process::env_var_mutex, and into V8.
|
||||
RemoveFromEnvironment(env, param.empty(), param);
|
||||
}
|
||||
|
||||
bool EnvPermission::is_granted(Environment* env,
|
||||
PermissionScope perm,
|
||||
std::string_view param) const {
|
||||
if (param.empty()) return granted_all();
|
||||
RwLock::ScopedReadLock lock(lock_);
|
||||
if (!dropped_.empty() && dropped_.contains(NormalizeEnvName(param))) {
|
||||
return false;
|
||||
}
|
||||
if (granted_all()) return true;
|
||||
return IsRuntimeEnvironmentDefault(param) ||
|
||||
(!dropped_all_ && MatchesAnyPattern(patterns_, param));
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
namespace {
|
||||
|
||||
bool IsAllDigits(std::string_view s) {
|
||||
return !s.empty() && std::all_of(s.begin(), s.end(), [](char c) {
|
||||
return c >= '0' && c <= '9';
|
||||
});
|
||||
}
|
||||
|
||||
// Whether `rest`, the part of a path after "<procfs>/<pid>/", names the
|
||||
// environ file of that process or of one of its threads.
|
||||
bool IsEnvironEntry(std::string_view rest) {
|
||||
if (rest == "environ") return true;
|
||||
constexpr std::string_view kTask = "task/";
|
||||
if (!rest.starts_with(kTask)) return false;
|
||||
rest.remove_prefix(kTask.size());
|
||||
const size_t slash = rest.find('/');
|
||||
return slash != std::string_view::npos &&
|
||||
IsAllDigits(rest.substr(0, slash)) &&
|
||||
rest.substr(slash + 1) == "environ";
|
||||
}
|
||||
|
||||
// Whether the absolute `path` names this process's own environ file, for
|
||||
// procfs mounted at /proc. `lexical` also accepts the self and thread-self
|
||||
// links, for paths that were not canonicalized.
|
||||
bool IsOwnProcEnviron(std::string_view path, bool lexical) {
|
||||
const std::string pid_dir = "/proc/" + std::to_string(uv_os_getpid()) + "/";
|
||||
if (path.starts_with(pid_dir)) {
|
||||
return IsEnvironEntry(path.substr(pid_dir.size()));
|
||||
}
|
||||
if (lexical) {
|
||||
constexpr std::string_view kSelf = "/proc/self/";
|
||||
if (path.starts_with(kSelf)) {
|
||||
return IsEnvironEntry(path.substr(kSelf.size()));
|
||||
}
|
||||
if (path == "/proc/thread-self/environ") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsProcEnvironReadDenied(std::string_view path, bool allow_own) {
|
||||
const std::string raw(path);
|
||||
|
||||
// Only a file on procfs can be an environ file, however the path reaches
|
||||
// it. statfs() follows symbolic links, and is a single system call, so
|
||||
// canonicalizing the path is only paid for files that are on procfs.
|
||||
struct statfs fs_info;
|
||||
if (statfs(raw.c_str(), &fs_info) == 0) {
|
||||
if (fs_info.f_type != PROC_SUPER_MAGIC) return false;
|
||||
char* real = realpath(raw.c_str(), nullptr);
|
||||
// Fail closed: a file on procfs whose path cannot be canonicalized is
|
||||
// treated as an environ file.
|
||||
if (real == nullptr) return true;
|
||||
const std::string canonical(real);
|
||||
free(real);
|
||||
// procfs has no other files named environ. It may be mounted somewhere
|
||||
// other than /proc, in which case the file is never recognized as this
|
||||
// process's own, and so is always denied.
|
||||
if (canonical != "environ" && !canonical.ends_with("/environ")) {
|
||||
return false;
|
||||
}
|
||||
return !(allow_own && IsOwnProcEnviron(canonical, false));
|
||||
}
|
||||
|
||||
// The file could not be examined, for example because it does not exist.
|
||||
// Fall back to the path as given, which can still name a file that comes
|
||||
// into existence before it is opened.
|
||||
if (!raw.starts_with("/proc/") || !raw.ends_with("/environ")) return false;
|
||||
return !(allow_own && IsOwnProcEnviron(raw, true));
|
||||
}
|
||||
#endif // defined(__linux__)
|
||||
|
||||
void EnvPermission::RemoveFromEnvironment(Environment* env,
|
||||
bool drop_all,
|
||||
std::string_view name) {
|
||||
if (env == nullptr) return;
|
||||
// An Environment that does not own the process state shares the real
|
||||
// process environment with the embedder, and must not modify it.
|
||||
const bool is_process_environment =
|
||||
env->env_vars() == per_process::system_environment;
|
||||
if (is_process_environment && !env->owns_process_state()) return;
|
||||
|
||||
Isolate* isolate = env->isolate();
|
||||
HandleScope handle_scope(isolate);
|
||||
Local<Context> context = env->context();
|
||||
|
||||
// Collected first, so that the entries the process started with can be
|
||||
// located while `environ` still refers to them.
|
||||
LocalVector<String> keys_to_remove(isolate);
|
||||
std::vector<std::string> removed;
|
||||
|
||||
if (!drop_all) {
|
||||
Local<Value> key;
|
||||
if (!ToV8Value(context, name, isolate).ToLocal(&key) || !key->IsString()) {
|
||||
return;
|
||||
}
|
||||
keys_to_remove.emplace_back(key.As<String>());
|
||||
removed.emplace_back(name);
|
||||
} else {
|
||||
Local<Array> keys;
|
||||
if (!env->env_vars()->Enumerate(isolate).ToLocal(&keys)) return;
|
||||
const uint32_t length = keys->Length();
|
||||
for (uint32_t i = 0; i < length; i++) {
|
||||
Local<Value> key;
|
||||
if (!keys->Get(context, i).ToLocal(&key) || !key->IsString()) continue;
|
||||
Utf8Value key_utf8(isolate, key);
|
||||
if (IsRuntimeEnvironmentDefault(key_utf8.ToStringView())) continue;
|
||||
keys_to_remove.emplace_back(key.As<String>());
|
||||
removed.emplace_back(key_utf8.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// Only the real process environment has an initial block behind it. The
|
||||
// KVStore methods below take per_process::env_var_mutex themselves, so it
|
||||
// must not be held across them.
|
||||
InitialEnvEntries initial_entries;
|
||||
if (is_process_environment) {
|
||||
Mutex::ScopedLock env_lock(per_process::env_var_mutex);
|
||||
initial_entries = FindInitialEnvironmentEntries(removed);
|
||||
}
|
||||
|
||||
for (Local<String> key : keys_to_remove) {
|
||||
env->env_vars()->Delete(isolate, key);
|
||||
#ifdef _WIN32
|
||||
if (is_process_environment) {
|
||||
UnsetCrtEnvironmentVariable(Utf8Value(isolate, key).ToString());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// As in ScrubProcessEnvironment(), overwrite the values that unsetenv()
|
||||
// leaves behind in the initial environment block, which /proc/<pid>/environ
|
||||
// would otherwise still expose.
|
||||
WipeInitialEnvironmentEntries(initial_entries);
|
||||
}
|
||||
|
||||
} // namespace permission
|
||||
|
||||
} // namespace node
|
||||
@@ -0,0 +1,138 @@
|
||||
#ifndef SRC_PERMISSION_ENV_PERMISSION_H_
|
||||
#define SRC_PERMISSION_ENV_PERMISSION_H_
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
|
||||
#include "node_mutex.h"
|
||||
#include "permission/permission_base.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace node {
|
||||
|
||||
class Environment;
|
||||
|
||||
namespace permission {
|
||||
|
||||
// Splits the values of --allow-env (which may be given more than once, and
|
||||
// may each hold a comma-separated list) into individual patterns.
|
||||
std::vector<std::string> ParseEnvAllowList(std::span<const std::string> values);
|
||||
|
||||
// Whether `pattern` is `*`, a variable name, or a variable name prefix
|
||||
// followed by `*`.
|
||||
bool IsValidEnvAllowPattern(std::string_view pattern);
|
||||
|
||||
// Whether `name` matches `pattern`: `*`, an exact name, or a prefix followed
|
||||
// by `*`. Names are compared case-insensitively on Windows, where environment
|
||||
// variable names are case-insensitive.
|
||||
bool EnvNameMatchesPattern(std::string_view pattern, std::string_view name);
|
||||
|
||||
// Returns the patterns matching exactly the names that both `a` and `b` match.
|
||||
std::vector<std::string> IntersectEnvAllowLists(std::span<const std::string> a,
|
||||
std::span<const std::string> b);
|
||||
|
||||
// Environment variables that Node.js and its bundled dependencies read after
|
||||
// startup. The scrub keeps them, and dropping the whole env scope keeps them.
|
||||
std::span<const std::string_view> GetRuntimeEnvironmentDefaults();
|
||||
bool IsRuntimeEnvironmentDefault(std::string_view name);
|
||||
|
||||
struct EnvScrubOptions {
|
||||
// Names or patterns to keep, see EnvNameMatchesPattern().
|
||||
std::span<const std::string> allow;
|
||||
// Whether to keep GetRuntimeEnvironmentDefaults().
|
||||
bool keep_runtime_defaults = true;
|
||||
// Whether to overwrite the removed entries in the environment block the
|
||||
// process started with, which /proc/<pid>/environ still exposes after
|
||||
// unsetenv(). Best effort, and currently only implemented on Linux.
|
||||
bool wipe_initial_block = true;
|
||||
};
|
||||
|
||||
// Removes every environment variable that `options` does not keep from the
|
||||
// process environment, and returns the names it removed.
|
||||
//
|
||||
// This modifies the process environment without any locking that native
|
||||
// code calling getenv() participates in, so it must be called before any
|
||||
// such thread is started.
|
||||
std::vector<std::string> ScrubProcessEnvironment(
|
||||
const EnvScrubOptions& options);
|
||||
|
||||
// Returns the names of the variables in the process environment that neither
|
||||
// `allow` nor GetRuntimeEnvironmentDefaults() match.
|
||||
std::vector<std::string> FindDeniedEnvironmentVariables(
|
||||
std::span<const std::string> allow);
|
||||
|
||||
// In audit mode, nothing is removed. Records the names of the variables that
|
||||
// ScrubProcessEnvironment() would remove instead.
|
||||
void RecordAuditedEnvironmentVariables(std::span<const std::string> allow);
|
||||
|
||||
// Whether ScrubProcessEnvironment() has run in this process.
|
||||
bool IsProcessEnvironmentScrubbed();
|
||||
|
||||
// Whether `name` was removed by ScrubProcessEnvironment().
|
||||
bool WasRemovedByEnvironmentScrub(std::string_view name);
|
||||
|
||||
// Whether `name` was removed by ScrubProcessEnvironment(), or recorded by
|
||||
// RecordAuditedEnvironmentVariables().
|
||||
bool WasDeniedAtStartup(std::string_view name);
|
||||
|
||||
// Returns true only the first time it is called for a given `name`.
|
||||
bool ShouldWarnAboutRemovedEnvVar(std::string_view name);
|
||||
|
||||
class EnvPermission final : public PermissionBase {
|
||||
public:
|
||||
void Apply(Environment* env,
|
||||
std::span<const std::string> allow,
|
||||
PermissionScope scope) override;
|
||||
void Drop(Environment* env,
|
||||
PermissionScope scope,
|
||||
std::string_view param) override;
|
||||
bool is_granted(Environment* env,
|
||||
PermissionScope perm,
|
||||
std::string_view param) const override;
|
||||
|
||||
// Whether every environment variable is accessible. Lock-free, so that the
|
||||
// file system scope can consult it on every check, from any thread. A check
|
||||
// that races with Drop() on another thread may observe the value from
|
||||
// before the drop.
|
||||
bool granted_all() const {
|
||||
return granted_all_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
private:
|
||||
// Removes the variables `drop_all` or `name` refer to from the
|
||||
// environment `env` exposes as process.env.
|
||||
void RemoveFromEnvironment(Environment* env,
|
||||
bool drop_all,
|
||||
std::string_view name);
|
||||
|
||||
std::atomic<bool> granted_all_{false};
|
||||
// Guards the members below. is_granted() may be called from threads other
|
||||
// than the one that owns the Environment, such as the thread pool.
|
||||
mutable RwLock lock_;
|
||||
bool dropped_all_ = false;
|
||||
std::vector<std::string> patterns_;
|
||||
std::unordered_set<std::string> dropped_;
|
||||
};
|
||||
|
||||
#if defined(__linux__)
|
||||
// Whether reading the file at `path` must be denied because it is, or
|
||||
// resolves to, the /proc/<pid>/environ file of a process, which exposes the
|
||||
// environment that process started with. Every process's file is denied
|
||||
// except this process's own, which is allowed only when `allow_own` is set.
|
||||
// Symbolic links are resolved first, so that paths such as
|
||||
// /dev/fd/../environ are recognized. `path` may be relative to the current
|
||||
// working directory.
|
||||
bool IsProcEnvironReadDenied(std::string_view path, bool allow_own);
|
||||
#endif // defined(__linux__)
|
||||
|
||||
} // namespace permission
|
||||
|
||||
} // namespace node
|
||||
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
#endif // SRC_PERMISSION_ENV_PERMISSION_H_
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "node_file.h"
|
||||
|
||||
#include "permission/boolean_permission.h"
|
||||
#include "permission/env_permission.h"
|
||||
#include "permission/fs_permission.h"
|
||||
#include "permission/permission_base.h"
|
||||
#include "v8-fast-api-calls.h"
|
||||
@@ -58,6 +59,8 @@ constexpr std::string_view GetDiagnosticsChannelName(PermissionScope scope) {
|
||||
return "node:permission-model:ffi";
|
||||
case PermissionScope::kOpenSSLStore:
|
||||
return "node:permission-model:openssl-store";
|
||||
case PermissionScope::kEnv:
|
||||
return "node:permission-model:env";
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@@ -77,6 +80,18 @@ Local<DictionaryTemplate> GetPermissionDiagnosticsTemplate(Environment* env) {
|
||||
return tmpl;
|
||||
}
|
||||
|
||||
// Returns true if the permission model removed the environment variable
|
||||
// named by args[0] at startup and no warning about it has been emitted yet,
|
||||
// and records that one has been. For callers that emit a more specific
|
||||
// warning than the one reading the variable from process.env would.
|
||||
static void TakeRemovedEnvVarWarning(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
CHECK(args[0]->IsString());
|
||||
Utf8Value name(env->isolate(), args[0]);
|
||||
args.GetReturnValue().Set(WasRemovedByEnvironmentScrub(name.ToStringView()) &&
|
||||
ShouldWarnAboutRemovedEnvVar(name.ToStringView()));
|
||||
}
|
||||
|
||||
// permission.drop('fs.read', '/tmp/')
|
||||
// permission.drop('child')
|
||||
static void Drop(const FunctionCallbackInfo<Value>& args) {
|
||||
@@ -89,6 +104,18 @@ static void Drop(const FunctionCallbackInfo<Value>& args) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dropping environment variables removes them from the environment. An
|
||||
// Environment that does not own the process state shares the real process
|
||||
// environment with the embedder, and must not modify it.
|
||||
if (scope == PermissionScope::kEnv &&
|
||||
env->env_vars() == per_process::system_environment &&
|
||||
!env->owns_process_state()) {
|
||||
return THROW_ERR_INVALID_STATE(
|
||||
env,
|
||||
"Environment variables can only be dropped from an Environment that "
|
||||
"owns the process state");
|
||||
}
|
||||
|
||||
if (args.Length() > 1 && !args[1]->IsUndefined()) {
|
||||
// BufferValue copies raw bytes out of a Buffer/TypedArray as-is instead
|
||||
// of forcing a (potentially lossy) UTF-8 string conversion, since paths
|
||||
@@ -241,6 +268,11 @@ Permission::Permission() : enabled_(false), warning_only_(false) {
|
||||
nodes_[static_cast<size_t>(PermissionScope::k##Name)] = \
|
||||
std::make_shared<AllowRevokePermission>();
|
||||
NET_PERMISSIONS(V)
|
||||
#undef V
|
||||
env_permission_ = std::make_shared<EnvPermission>();
|
||||
#define V(Name, _, __, ___) \
|
||||
nodes_[static_cast<size_t>(PermissionScope::k##Name)] = env_permission_;
|
||||
ENV_PERMISSIONS(V)
|
||||
#undef V
|
||||
}
|
||||
|
||||
@@ -320,7 +352,21 @@ bool Permission::is_granted_quiet(Environment* env,
|
||||
CHECK(permission != PermissionScope::kPermissionsRoot &&
|
||||
permission != PermissionScope::kPermissionsCount);
|
||||
auto& perm_node = nodes_[static_cast<size_t>(permission)];
|
||||
return perm_node && perm_node->is_granted(env, permission, res);
|
||||
if (!perm_node || !perm_node->is_granted(env, permission, res)) {
|
||||
return false;
|
||||
}
|
||||
#if defined(__linux__)
|
||||
// /proc/<pid>/environ exposes the environment a process was started with,
|
||||
// including variables that the env scope does not grant access to. Other
|
||||
// processes' files are always denied: a child process is granted every
|
||||
// variable in the environment its parent hands it, and must not reach the
|
||||
// environments the parent could not.
|
||||
if (permission == PermissionScope::kFileSystemRead && !res.empty() &&
|
||||
IsProcEnvironReadDenied(res, env_permission_->granted_all())) {
|
||||
return false;
|
||||
}
|
||||
#endif // defined(__linux__)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Permission::is_scope_granted(Environment* env,
|
||||
@@ -406,6 +452,8 @@ void Initialize(Local<Object> target,
|
||||
SetFastMethodNoSideEffect(
|
||||
context, target, "has", Has, {fast_has_methods_, 2});
|
||||
SetMethod(context, target, "drop", Drop);
|
||||
SetMethod(
|
||||
context, target, "takeRemovedEnvVarWarning", TakeRemovedEnvVarWarning);
|
||||
|
||||
target->SetIntegrityLevel(context, IntegrityLevel::kFrozen).FromJust();
|
||||
}
|
||||
@@ -416,6 +464,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||
registry->Register(method);
|
||||
}
|
||||
registry->Register(Drop);
|
||||
registry->Register(TakeRemovedEnvVarWarning);
|
||||
}
|
||||
|
||||
} // namespace permission
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "debug_utils.h"
|
||||
#include "node_diagnostics_channel.h"
|
||||
#include "permission/env_permission.h"
|
||||
#include "permission/permission_base.h"
|
||||
|
||||
#include <array>
|
||||
@@ -150,6 +151,8 @@ class Permission {
|
||||
static_cast<size_t>(PermissionScope::kPermissionsCount);
|
||||
|
||||
std::array<std::shared_ptr<PermissionBase>, kPermissionCount> nodes_;
|
||||
// Also stored in nodes_, kept here for the file system scope to consult.
|
||||
std::shared_ptr<EnvPermission> env_permission_;
|
||||
bool enabled_;
|
||||
bool warning_only_;
|
||||
mutable bool publishing_ = false;
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace permission {
|
||||
#define OPENSSL_STORE_PERMISSIONS(V) \
|
||||
V(OpenSSLStore, "openssl.store", PermissionsRoot, "--allow-openssl-store")
|
||||
|
||||
#define ENV_PERMISSIONS(V) V(Env, "env", PermissionsRoot, "--allow-env")
|
||||
|
||||
#define PERMISSIONS(V) \
|
||||
FILESYSTEM_PERMISSIONS(V) \
|
||||
CHILD_PROCESS_PERMISSIONS(V) \
|
||||
@@ -48,7 +50,8 @@ namespace permission {
|
||||
NET_PERMISSIONS(V) \
|
||||
ADDON_PERMISSIONS(V) \
|
||||
FFI_PERMISSIONS(V) \
|
||||
OPENSSL_STORE_PERMISSIONS(V)
|
||||
OPENSSL_STORE_PERMISSIONS(V) \
|
||||
ENV_PERMISSIONS(V)
|
||||
|
||||
#define V(name, _, __, ___) k##name,
|
||||
enum class PermissionScope {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "gtest/gtest.h"
|
||||
#include "node.h"
|
||||
#include "node_test_fixture.h"
|
||||
#include "permission/env_permission.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using node::permission::EnvNameMatchesPattern;
|
||||
using node::permission::GetRuntimeEnvironmentDefaults;
|
||||
using node::permission::IntersectEnvAllowLists;
|
||||
using node::permission::IsRuntimeEnvironmentDefault;
|
||||
using node::permission::IsValidEnvAllowPattern;
|
||||
using node::permission::ParseEnvAllowList;
|
||||
|
||||
TEST(EnvPermissionTest, ParseEnvAllowList) {
|
||||
const std::vector<std::string> values = {"A,B", "C", "", ",D,,E,"};
|
||||
EXPECT_EQ(ParseEnvAllowList(values),
|
||||
(std::vector<std::string>{"A", "B", "C", "D", "E"}));
|
||||
}
|
||||
|
||||
TEST(EnvPermissionTest, IsValidEnvAllowPattern) {
|
||||
EXPECT_TRUE(IsValidEnvAllowPattern("*"));
|
||||
EXPECT_TRUE(IsValidEnvAllowPattern("PATH"));
|
||||
EXPECT_TRUE(IsValidEnvAllowPattern("APP_*"));
|
||||
EXPECT_FALSE(IsValidEnvAllowPattern(""));
|
||||
EXPECT_FALSE(IsValidEnvAllowPattern("A=B"));
|
||||
EXPECT_FALSE(IsValidEnvAllowPattern("*A"));
|
||||
EXPECT_FALSE(IsValidEnvAllowPattern("A*B"));
|
||||
EXPECT_FALSE(IsValidEnvAllowPattern("A**"));
|
||||
}
|
||||
|
||||
TEST(EnvPermissionTest, EnvNameMatchesPattern) {
|
||||
EXPECT_TRUE(EnvNameMatchesPattern("*", "ANYTHING"));
|
||||
EXPECT_TRUE(EnvNameMatchesPattern("PATH", "PATH"));
|
||||
EXPECT_FALSE(EnvNameMatchesPattern("PATH", "PATHEXT"));
|
||||
EXPECT_FALSE(EnvNameMatchesPattern("PATHEXT", "PATH"));
|
||||
EXPECT_TRUE(EnvNameMatchesPattern("APP_*", "APP_"));
|
||||
EXPECT_TRUE(EnvNameMatchesPattern("APP_*", "APP_DB_URL"));
|
||||
EXPECT_FALSE(EnvNameMatchesPattern("APP_*", "APP"));
|
||||
EXPECT_FALSE(EnvNameMatchesPattern("APP_*", "OTHER_APP_DB"));
|
||||
#ifdef _WIN32
|
||||
EXPECT_TRUE(EnvNameMatchesPattern("path", "PATH"));
|
||||
EXPECT_TRUE(EnvNameMatchesPattern("app_*", "APP_DB_URL"));
|
||||
#else
|
||||
EXPECT_FALSE(EnvNameMatchesPattern("path", "PATH"));
|
||||
EXPECT_FALSE(EnvNameMatchesPattern("app_*", "APP_DB_URL"));
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(EnvPermissionTest, IntersectEnvAllowLists) {
|
||||
using List = std::vector<std::string>;
|
||||
EXPECT_EQ(IntersectEnvAllowLists(List{"*"}, List{"A", "B_*"}),
|
||||
(List{"A", "B_*"}));
|
||||
EXPECT_EQ(IntersectEnvAllowLists(List{"A", "B"}, List{"*"}),
|
||||
(List{"A", "B"}));
|
||||
EXPECT_EQ(IntersectEnvAllowLists(List{"A", "B"}, List{"B", "C"}),
|
||||
(List{"B"}));
|
||||
EXPECT_EQ(IntersectEnvAllowLists(List{"APP_*"}, List{"APP_DB*", "OTHER"}),
|
||||
(List{"APP_DB*"}));
|
||||
EXPECT_EQ(IntersectEnvAllowLists(List{"APP_DB_URL"}, List{"APP_*"}),
|
||||
(List{"APP_DB_URL"}));
|
||||
EXPECT_EQ(IntersectEnvAllowLists(List{"APP_*"}, List{"OTHER_*"}), List{});
|
||||
EXPECT_EQ(IntersectEnvAllowLists(List{}, List{"*"}), List{});
|
||||
}
|
||||
|
||||
TEST(EnvPermissionTest, RuntimeEnvironmentDefaults) {
|
||||
EXPECT_TRUE(IsRuntimeEnvironmentDefault("NODE_OPTIONS"));
|
||||
EXPECT_TRUE(IsRuntimeEnvironmentDefault("TZ"));
|
||||
EXPECT_TRUE(IsRuntimeEnvironmentDefault("LC_ALL"));
|
||||
EXPECT_FALSE(IsRuntimeEnvironmentDefault("NODE_AUTH_TOKEN"));
|
||||
EXPECT_FALSE(IsRuntimeEnvironmentDefault("HTTP_PROXY"));
|
||||
EXPECT_EQ(node::GetRuntimeEnvironmentDefaults().size(),
|
||||
GetRuntimeEnvironmentDefaults().size());
|
||||
}
|
||||
|
||||
class EnvPermissionScrubTest : public NodeZeroIsolateTestFixture {};
|
||||
|
||||
TEST_F(EnvPermissionScrubTest, ScrubProcessEnvironmentAfterInitialization) {
|
||||
// The fixture has already initialized Node.js, so modifying the process
|
||||
// environment is no longer safe.
|
||||
EXPECT_TRUE(node::ScrubProcessEnvironment({}).IsNothing());
|
||||
}
|
||||
@@ -100,6 +100,28 @@ NODE_MAIN(int argc, node::argv_type raw_argv[]) {
|
||||
args.erase(it);
|
||||
flags |= node::ProcessInitializationFlags::kNoHarvestBuiltinCodeCache;
|
||||
}
|
||||
// --embedder-scrub-env=<names>: remove the environment variables other than
|
||||
// the comma-separated <names> before initializing Node.js.
|
||||
static constexpr std::string_view kScrubEnvFlag = "--embedder-scrub-env=";
|
||||
it = std::find_if(args.begin(), args.end(), [](const std::string& arg) {
|
||||
return arg.starts_with(kScrubEnvFlag);
|
||||
});
|
||||
if (it != args.end()) {
|
||||
node::ProcessEnvironmentScrubOptions scrub_options;
|
||||
const std::string names = it->substr(kScrubEnvFlag.size());
|
||||
for (size_t begin = 0; begin < names.size();) {
|
||||
size_t end = names.find(',', begin);
|
||||
if (end == std::string::npos) end = names.size();
|
||||
scrub_options.allow.push_back(names.substr(begin, end - begin));
|
||||
begin = end + 1;
|
||||
}
|
||||
args.erase(it);
|
||||
if (node::ScrubProcessEnvironment(scrub_options).IsNothing()) {
|
||||
fprintf(
|
||||
stderr, "%s: ScrubProcessEnvironment() failed\n", args[0].c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
std::shared_ptr<node::InitializationResult> result =
|
||||
node::InitializeOncePerProcess(
|
||||
args, static_cast<node::ProcessInitializationFlags::Flags>(flags));
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
// Tests node::ScrubProcessEnvironment(), and that embedders that enable the
|
||||
// permission model must remove the environment variables --allow-env does not
|
||||
// grant access to before node::InitializeOncePerProcess().
|
||||
|
||||
const common = require('../common');
|
||||
const { spawnSyncAndAssert, spawnSyncAndExit } = require('../common/child_process');
|
||||
|
||||
const embedtest = common.resolveBuiltBinary('embedtest');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PERMISSION_ENV_SECRET: 'secret',
|
||||
PERMISSION_ENV_ALLOWED: 'allowed',
|
||||
};
|
||||
|
||||
const script = 'console.log(JSON.stringify([' +
|
||||
'process.env.PERMISSION_ENV_SECRET, process.env.PERMISSION_ENV_ALLOWED]))';
|
||||
|
||||
const permissionFlags = [
|
||||
'--permission',
|
||||
'--allow-fs-read=*',
|
||||
'--allow-env=PERMISSION_ENV_ALLOWED',
|
||||
];
|
||||
|
||||
// Initialization fails while the environment contains denied variables.
|
||||
// The message lists at most five of them, so the child gets a minimal
|
||||
// environment. It still needs the dynamic loader's search path, e.g. when
|
||||
// built against a shared OpenSSL that lives outside the default search path.
|
||||
// The child's environment is not fully under the test's control either: macOS
|
||||
// adds __CF_USER_TEXT_ENCODING to it, so the message may list more than the
|
||||
// variables set here.
|
||||
const minimalEnv = {
|
||||
PERMISSION_ENV_SECRET: 'secret',
|
||||
PERMISSION_ENV_ALLOWED: 'allowed',
|
||||
};
|
||||
let loaderPathVar = 'LD_LIBRARY_PATH';
|
||||
if (common.isWindows) loaderPathVar = 'PATH';
|
||||
else if (common.isMacOS) loaderPathVar = 'DYLD_LIBRARY_PATH';
|
||||
else if (common.isAIX) loaderPathVar = 'LIBPATH';
|
||||
if (process.env[loaderPathVar] !== undefined) {
|
||||
minimalEnv[loaderPathVar] = process.env[loaderPathVar];
|
||||
}
|
||||
spawnSyncAndExit(
|
||||
embedtest,
|
||||
[...permissionFlags, script],
|
||||
{ env: minimalEnv },
|
||||
{
|
||||
status: 9,
|
||||
signal: null,
|
||||
stderr: /The process environment contains variables that --allow-env does not grant access to \([^)]*\bPERMISSION_ENV_SECRET\b[^)]*\)\. Remove them with node::ScrubProcessEnvironment\(\) before calling node::InitializeOncePerProcess\(\)\./,
|
||||
});
|
||||
|
||||
// It succeeds once they have been removed.
|
||||
spawnSyncAndAssert(
|
||||
embedtest,
|
||||
['--embedder-scrub-env=PERMISSION_ENV_ALLOWED', ...permissionFlags, script],
|
||||
{ env },
|
||||
{
|
||||
trim: true,
|
||||
stdout: '[null,"allowed"]',
|
||||
});
|
||||
|
||||
// With --allow-env=*, nothing needs to be removed.
|
||||
spawnSyncAndAssert(
|
||||
embedtest,
|
||||
['--permission', '--allow-fs-read=*', '--allow-env=*', script],
|
||||
{ env },
|
||||
{
|
||||
trim: true,
|
||||
stdout: '["secret","allowed"]',
|
||||
});
|
||||
|
||||
// ScrubProcessEnvironment() rejects invalid patterns.
|
||||
spawnSyncAndExit(
|
||||
embedtest,
|
||||
['--embedder-scrub-env=PERMISSION_*_ENV', script],
|
||||
{ env },
|
||||
{
|
||||
status: 1,
|
||||
signal: null,
|
||||
stderr: /ScrubProcessEnvironment\(\) failed/,
|
||||
});
|
||||
@@ -413,9 +413,12 @@ function assertPermissionModel() {
|
||||
});
|
||||
`;
|
||||
|
||||
// SoftHSM locates its configuration through SOFTHSM2_CONF, which the
|
||||
// permission model would otherwise scrub from the environment at startup.
|
||||
assertChild([
|
||||
`--openssl-config=${kOpenSSLConfig}`,
|
||||
'--permission',
|
||||
'--allow-env=SOFTHSM2_CONF',
|
||||
'--allow-fs-read=*',
|
||||
'-e',
|
||||
code,
|
||||
@@ -424,6 +427,7 @@ function assertPermissionModel() {
|
||||
assertChild([
|
||||
`--openssl-config=${kOpenSSLConfig}`,
|
||||
'--permission',
|
||||
'--allow-env=SOFTHSM2_CONF',
|
||||
'--allow-openssl-store',
|
||||
'--allow-fs-read=*',
|
||||
'-e',
|
||||
|
||||
@@ -28,6 +28,7 @@ assert(expected.includes(path.join('a', 'b', '2')));
|
||||
|
||||
const { status, stderr } = spawnSync(process.execPath, [
|
||||
'--permission',
|
||||
'--allow-env=ALLOWED,BLOCKED,EXPECTED',
|
||||
`--allow-fs-read=${allowed}`,
|
||||
'-e',
|
||||
`
|
||||
|
||||
@@ -48,7 +48,7 @@ function run(flag, { op, name }) {
|
||||
};
|
||||
const { status, stdout, stderr } = spawnSync(
|
||||
process.execPath,
|
||||
[flag, '-e', childScript],
|
||||
[flag, '--allow-env=BLOCKED_FILE,LINK_PATH', '-e', childScript],
|
||||
{ encoding: 'utf8', env },
|
||||
);
|
||||
assert.strictEqual(status, 0, stderr);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
if (process.config.variables.node_without_node_options) {
|
||||
common.skip('missing NODE_OPTIONS support');
|
||||
}
|
||||
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PERMISSION_ENV_SECRET: 'secret',
|
||||
PERMISSION_ENV_ALLOWED: 'allowed',
|
||||
};
|
||||
|
||||
// The environment of a child spawned by a process that enforces the
|
||||
// permission model already reflects --allow-env, so the child gets access to
|
||||
// all of it: including variables the parent added, and ones added for the
|
||||
// child only. It still cannot see what the parent removed.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-child-process', '--no-warnings',
|
||||
'--allow-env=PERMISSION_ENV_ALLOWED', '-e',
|
||||
`
|
||||
const assert = require('assert');
|
||||
const { spawnSync } = require('child_process');
|
||||
process.env.PERMISSION_ENV_RUNTIME = 'runtime';
|
||||
const { status, stdout, stderr } = spawnSync(process.execPath, [
|
||||
'--no-warnings', '-p',
|
||||
'JSON.stringify([' +
|
||||
'process.env.PERMISSION_ENV_ALLOWED, ' +
|
||||
'process.env.PERMISSION_ENV_RUNTIME, ' +
|
||||
'process.env.PERMISSION_ENV_CHILD, ' +
|
||||
'process.env.PERMISSION_ENV_SECRET, ' +
|
||||
'process.permission.has("env")])',
|
||||
], {
|
||||
env: { ...process.env, PERMISSION_ENV_CHILD: 'child' },
|
||||
});
|
||||
assert.strictEqual(status, 0, stderr.toString());
|
||||
assert.deepStrictEqual(
|
||||
JSON.parse(stdout),
|
||||
['allowed', 'runtime', 'child', null, true]);
|
||||
`,
|
||||
],
|
||||
{ env },
|
||||
{});
|
||||
|
||||
// In audit mode nothing was removed, so children audit the same policy.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission-audit', '--allow-child-process', '--no-warnings',
|
||||
'--allow-env=PERMISSION_ENV_ALLOWED', '-e',
|
||||
`
|
||||
const assert = require('assert');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { status, stdout, stderr } = spawnSync(process.execPath, [
|
||||
'--no-warnings', '-p',
|
||||
'JSON.stringify([' +
|
||||
'process.permission.has("env"), ' +
|
||||
'process.permission.has("env", "PERMISSION_ENV_ALLOWED"), ' +
|
||||
'process.permission.has("env", "PERMISSION_ENV_SECRET"), ' +
|
||||
'process.env.PERMISSION_ENV_SECRET])',
|
||||
]);
|
||||
assert.strictEqual(status, 0, stderr.toString());
|
||||
assert.deepStrictEqual(JSON.parse(stdout), [false, true, false, 'secret']);
|
||||
`,
|
||||
],
|
||||
{ env },
|
||||
{});
|
||||
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
const assert = require('assert');
|
||||
const { spawnSyncAndAssert, spawnSyncAndExit } = require('../common/child_process');
|
||||
|
||||
for (const value of ['A*B', '*A', 'A=B', 'A,B*C']) {
|
||||
spawnSyncAndExit(
|
||||
process.execPath,
|
||||
['--permission', `--allow-env=${value}`, '-e', ''],
|
||||
{
|
||||
status: 9,
|
||||
signal: null,
|
||||
stderr: /--allow-env must be '\*', a variable name, or a variable name prefix followed by '\*'/,
|
||||
});
|
||||
}
|
||||
|
||||
// --allow-env is accepted in NODE_OPTIONS.
|
||||
if (!process.config.variables.node_without_node_options) {
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['--permission', '-p', 'process.env.PERMISSION_ENV_ALLOWED'],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_OPTIONS: '--allow-env=PERMISSION_ENV_ALLOWED',
|
||||
PERMISSION_ENV_ALLOWED: 'allowed',
|
||||
},
|
||||
},
|
||||
{
|
||||
stdout(output) {
|
||||
assert.strictEqual(output.trim(), 'allowed');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
|
||||
tmpdir.refresh();
|
||||
|
||||
function writeConfig(name, allowEnv) {
|
||||
const path = tmpdir.resolve(name);
|
||||
fs.writeFileSync(path, JSON.stringify({ permission: { 'allow-env': allowEnv } }));
|
||||
return path;
|
||||
}
|
||||
|
||||
const bothConfig = writeConfig('both.json', ['PERMISSION_ENV_A', 'PERMISSION_ENV_B']);
|
||||
const allConfig = writeConfig('all.json', ['*']);
|
||||
const prefixConfig = writeConfig('prefix.json', ['PERMISSION_ENV_PREFIX_DB*']);
|
||||
|
||||
const envFile = tmpdir.resolve('options.env');
|
||||
fs.writeFileSync(envFile, 'NODE_OPTIONS="--allow-env=*"\n');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PERMISSION_ENV_A: 'a',
|
||||
PERMISSION_ENV_B: 'b',
|
||||
PERMISSION_ENV_PREFIX_DB_URL: 'db',
|
||||
PERMISSION_ENV_PREFIX_OTHER: 'other',
|
||||
};
|
||||
|
||||
function visible(flags, extraEnv = {}) {
|
||||
let result;
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
...flags, '--no-warnings', '-p',
|
||||
'JSON.stringify(Object.keys(process.env).filter((name) => name.startsWith("PERMISSION_ENV_")).sort())',
|
||||
],
|
||||
{ env: { ...env, ...extraEnv } },
|
||||
{
|
||||
stdout(output) {
|
||||
result = JSON.parse(output);
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// When the command line enables the permission model, --allow-env values from
|
||||
// the configuration file can only narrow what the command line grants.
|
||||
assert.deepStrictEqual(
|
||||
visible([
|
||||
'--permission',
|
||||
'--allow-env=PERMISSION_ENV_A',
|
||||
`--experimental-config-file=${bothConfig}`,
|
||||
]),
|
||||
['PERMISSION_ENV_A']);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
visible(['--permission', `--experimental-config-file=${allConfig}`]),
|
||||
[]);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
visible([
|
||||
'--permission',
|
||||
'--allow-env=PERMISSION_ENV_PREFIX_*',
|
||||
`--experimental-config-file=${prefixConfig}`,
|
||||
]),
|
||||
['PERMISSION_ENV_PREFIX_DB_URL']);
|
||||
|
||||
// The same applies to NODE_OPTIONS defined in an env file.
|
||||
assert.deepStrictEqual(
|
||||
visible([
|
||||
'--permission',
|
||||
'--allow-env=PERMISSION_ENV_A',
|
||||
`--env-file=${envFile}`,
|
||||
]),
|
||||
['PERMISSION_ENV_A']);
|
||||
|
||||
// When only the configuration file enables the permission model, its
|
||||
// --allow-env values apply.
|
||||
assert.deepStrictEqual(
|
||||
visible([`--experimental-config-file=${bothConfig}`]),
|
||||
['PERMISSION_ENV_A', 'PERMISSION_ENV_B']);
|
||||
|
||||
// The NODE_OPTIONS environment variable is not a file, and can grant access.
|
||||
if (!process.config.variables.node_without_node_options) {
|
||||
assert.deepStrictEqual(
|
||||
visible(
|
||||
['--permission', '--allow-env=PERMISSION_ENV_A'],
|
||||
{ NODE_OPTIONS: '--allow-env=PERMISSION_ENV_B' }),
|
||||
['PERMISSION_ENV_A', 'PERMISSION_ENV_B']);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PERMISSION_ENV_ONE: 'one',
|
||||
PERMISSION_ENV_TWO: 'two',
|
||||
TZ: 'UTC',
|
||||
};
|
||||
|
||||
// Dropping a variable removes it from the environment.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-env=PERMISSION_ENV_ONE,PERMISSION_ENV_TWO', '-e',
|
||||
`
|
||||
const assert = require('assert');
|
||||
const dc = require('diagnostics_channel');
|
||||
const drops = [];
|
||||
dc.subscribe('node:permission-model:env', (message) => {
|
||||
if (message.drop) drops.push(message.resource);
|
||||
});
|
||||
|
||||
assert.strictEqual(process.permission.has('env', 'PERMISSION_ENV_ONE'), true);
|
||||
process.permission.drop('env', 'PERMISSION_ENV_ONE');
|
||||
assert.strictEqual(process.permission.has('env', 'PERMISSION_ENV_ONE'), false);
|
||||
assert.strictEqual(process.env.PERMISSION_ENV_ONE, undefined);
|
||||
assert.strictEqual(process.permission.has('env', 'PERMISSION_ENV_TWO'), true);
|
||||
assert.strictEqual(process.env.PERMISSION_ENV_TWO, 'two');
|
||||
|
||||
// Dropping the whole scope removes everything except the variables
|
||||
// Node.js reads itself.
|
||||
process.permission.drop('env');
|
||||
assert.strictEqual(process.permission.has('env', 'PERMISSION_ENV_TWO'), false);
|
||||
assert.strictEqual(process.env.PERMISSION_ENV_TWO, undefined);
|
||||
assert.strictEqual(process.permission.has('env', 'TZ'), true);
|
||||
assert.strictEqual(process.env.TZ, 'UTC');
|
||||
|
||||
assert.deepStrictEqual(drops, ['PERMISSION_ENV_ONE', '']);
|
||||
`,
|
||||
],
|
||||
{ env },
|
||||
{});
|
||||
|
||||
// Dropping a variable when every variable is accessible.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-env=*', '-e',
|
||||
`
|
||||
const assert = require('assert');
|
||||
assert.strictEqual(process.permission.has('env'), true);
|
||||
process.permission.drop('env', 'PERMISSION_ENV_ONE');
|
||||
assert.strictEqual(process.permission.has('env'), false);
|
||||
assert.strictEqual(process.permission.has('env', 'PERMISSION_ENV_ONE'), false);
|
||||
assert.strictEqual(process.env.PERMISSION_ENV_ONE, undefined);
|
||||
assert.strictEqual(process.permission.has('env', 'PERMISSION_ENV_TWO'), true);
|
||||
assert.strictEqual(process.env.PERMISSION_ENV_TWO, 'two');
|
||||
`,
|
||||
],
|
||||
{ env },
|
||||
{});
|
||||
@@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
|
||||
tmpdir.refresh();
|
||||
|
||||
const envFile = tmpdir.resolve('permission.env');
|
||||
fs.writeFileSync(
|
||||
envFile,
|
||||
'PERMISSION_ENV_FROM_FILE=file\nPERMISSION_ENV_BOTH=file\n');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PERMISSION_ENV_BOTH: 'inherited',
|
||||
};
|
||||
|
||||
const script = `console.log(JSON.stringify({
|
||||
fromFile: process.env.PERMISSION_ENV_FROM_FILE,
|
||||
both: process.env.PERMISSION_ENV_BOTH,
|
||||
has: [
|
||||
process.permission.has('env', 'PERMISSION_ENV_FROM_FILE'),
|
||||
process.permission.has('env', 'PERMISSION_ENV_BOTH'),
|
||||
],
|
||||
}))`;
|
||||
|
||||
// Variables defined in an env file are allowed. Only the file's values are
|
||||
// visible: an inherited value for the same name was removed at startup.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['--permission', `--env-file=${envFile}`, '-e', script],
|
||||
{ env },
|
||||
{
|
||||
stdout(output) {
|
||||
assert.deepStrictEqual(JSON.parse(output), {
|
||||
fromFile: 'file',
|
||||
both: 'file',
|
||||
has: [true, true],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// A name that --allow-env grants access to keeps the usual precedence of the
|
||||
// inherited value over the file's.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-env=PERMISSION_ENV_BOTH',
|
||||
`--env-file=${envFile}`,
|
||||
'-e', script,
|
||||
],
|
||||
{ env },
|
||||
{
|
||||
stdout(output) {
|
||||
assert.deepStrictEqual(JSON.parse(output), {
|
||||
fromFile: 'file',
|
||||
both: 'inherited',
|
||||
has: [true, true],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
if (!common.isLinux) {
|
||||
common.skip('/proc/<pid>/environ is specific to Linux');
|
||||
}
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const { spawn } = require('child_process');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PERMISSION_ENV_SECRET: 'secret-value',
|
||||
PERMISSION_ENV_ALLOWED: 'allowed-value',
|
||||
};
|
||||
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
tmpdir.refresh();
|
||||
|
||||
// A symbolic link that reaches the environ file of this process, which is
|
||||
// the parent of the processes below, under another name.
|
||||
const parentEnvironLink = tmpdir.resolve('parent-environ');
|
||||
fs.symlinkSync(`/proc/${process.pid}/environ`, parentEnvironLink);
|
||||
|
||||
// The source of an array of the paths that reach the environ file of the
|
||||
// parent process, evaluated in the child.
|
||||
const parentPaths = `[
|
||||
'/proc/' + process.ppid + '/environ',
|
||||
'/proc/' + process.ppid + '/task/' + process.ppid + '/environ',
|
||||
// Symbolic links resolve differently than the paths lexically do.
|
||||
'/dev/fd/../../' + process.ppid + '/environ',
|
||||
'/proc/self/root/proc/' + process.ppid + '/environ',
|
||||
${JSON.stringify(parentEnvironLink)},
|
||||
]`;
|
||||
|
||||
// The same for the environ file of the process itself.
|
||||
const ownPaths = `[
|
||||
'/proc/self/environ',
|
||||
'/proc/thread-self/environ',
|
||||
'/proc/' + process.pid + '/environ',
|
||||
'/proc/self/task/' + process.pid + '/environ',
|
||||
'/dev/fd/../environ',
|
||||
]`;
|
||||
|
||||
// Source that asserts, in the child, that reading each of `paths` is denied.
|
||||
const assertDenied = (paths) => `
|
||||
for (const path of ${paths}) {
|
||||
require('assert').throws(() => require('fs').readFileSync(path), {
|
||||
code: 'ERR_ACCESS_DENIED',
|
||||
permission: 'FileSystemRead',
|
||||
resource: path,
|
||||
}, path);
|
||||
}
|
||||
`;
|
||||
|
||||
// /proc/<pid>/environ exposes the environment a process started with, so
|
||||
// reading it is denied while the env scope is restricted, even when the file
|
||||
// system scope would allow it.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-fs-read=*', '-e',
|
||||
assertDenied(ownPaths) + assertDenied(parentPaths),
|
||||
],
|
||||
{ env },
|
||||
{});
|
||||
|
||||
// Relative paths are resolved against the working directory.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-fs-read=*', '--allow-env=*', '-e',
|
||||
assertDenied(`[
|
||||
process.ppid + '/environ',
|
||||
'/proc/self/cwd/' + process.ppid + '/environ',
|
||||
]`),
|
||||
],
|
||||
{ env, cwd: '/proc' },
|
||||
{});
|
||||
|
||||
// Reading its own is allowed when every variable is accessible, but the
|
||||
// environment of any other process stays out of reach: a child process is
|
||||
// started with --allow-env=*, and must not see what its parent could not.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-fs-read=*', '--allow-env=*', '-e',
|
||||
`
|
||||
for (const path of ${ownPaths}) require('fs').readFileSync(path);
|
||||
${assertDenied(parentPaths)}
|
||||
`,
|
||||
],
|
||||
{ env },
|
||||
{});
|
||||
|
||||
// The same holds for a child process that a restricted process spawns, which
|
||||
// inherits --allow-env=*.
|
||||
{
|
||||
const grandchild = `
|
||||
require('fs').readFileSync('/proc/self/environ');
|
||||
${assertDenied(`[
|
||||
'/proc/' + process.ppid + '/environ',
|
||||
'/dev/fd/../../' + process.ppid + '/environ',
|
||||
]`)}
|
||||
`;
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-fs-read=*', '--allow-child-process', '-e',
|
||||
`require('child_process').execFileSync(
|
||||
process.execPath, ['-e', ${JSON.stringify(grandchild)}],
|
||||
{ stdio: 'inherit' });`,
|
||||
],
|
||||
{ env },
|
||||
{});
|
||||
}
|
||||
|
||||
// Other files on procfs are not affected.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-fs-read=*', '-e',
|
||||
`
|
||||
const fs = require('fs');
|
||||
fs.readFileSync('/proc/self/stat');
|
||||
fs.readFileSync('/proc/' + process.ppid + '/stat');
|
||||
fs.readdirSync('/proc/self');
|
||||
`,
|
||||
],
|
||||
{ env },
|
||||
{});
|
||||
|
||||
// Other processes do not find the removed variables there either, because
|
||||
// they are overwritten in the initial environment block.
|
||||
{
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-env=PERMISSION_ENV_ALLOWED', '-e',
|
||||
'console.log("ready"); process.stdin.once("data", () => {});',
|
||||
],
|
||||
{ env, stdio: ['pipe', 'pipe', 'inherit'] });
|
||||
|
||||
child.stdout.once('data', common.mustCall(() => {
|
||||
const environ = fs.readFileSync(`/proc/${child.pid}/environ`, 'latin1');
|
||||
assert.doesNotMatch(environ, /PERMISSION_ENV_SECRET|secret-value/);
|
||||
assert.match(environ, /PERMISSION_ENV_ALLOWED=allowed-value/);
|
||||
child.stdin.end('done');
|
||||
}));
|
||||
|
||||
child.on('exit', common.mustCall((code, signal) => {
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}));
|
||||
}
|
||||
|
||||
// permission.drop() overwrites the initial environment block too, so a
|
||||
// variable that startup kept does not stay readable there after it is
|
||||
// dropped at runtime.
|
||||
{
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-env=PERMISSION_ENV_*', '-e',
|
||||
`
|
||||
console.log('ready');
|
||||
process.stdin.once('data', () => {
|
||||
process.permission.drop('env', 'PERMISSION_ENV_SECRET');
|
||||
console.log('dropped');
|
||||
process.stdin.once('data', () => {});
|
||||
});
|
||||
`,
|
||||
],
|
||||
{ env, stdio: ['pipe', 'pipe', 'inherit'] });
|
||||
|
||||
const readEnviron = () =>
|
||||
fs.readFileSync(`/proc/${child.pid}/environ`, 'latin1');
|
||||
|
||||
child.stdout.once('data', common.mustCall(() => {
|
||||
// Both are allowed at startup, so both are still in the initial block.
|
||||
const environ = readEnviron();
|
||||
assert.match(environ, /PERMISSION_ENV_SECRET=secret-value/);
|
||||
assert.match(environ, /PERMISSION_ENV_ALLOWED=allowed-value/);
|
||||
|
||||
child.stdout.once('data', common.mustCall(() => {
|
||||
const scrubbed = readEnviron();
|
||||
assert.doesNotMatch(scrubbed, /PERMISSION_ENV_SECRET|secret-value/);
|
||||
// Variables that were not dropped are left alone.
|
||||
assert.match(scrubbed, /PERMISSION_ENV_ALLOWED=allowed-value/);
|
||||
child.stdin.end('done');
|
||||
}));
|
||||
|
||||
child.stdin.write('drop');
|
||||
}));
|
||||
|
||||
child.on('exit', common.mustCall((code, signal) => {
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}));
|
||||
}
|
||||
|
||||
// Dropping the whole env scope does the same for every variable it removes.
|
||||
{
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-env=PERMISSION_ENV_*', '-e',
|
||||
`
|
||||
console.log('ready');
|
||||
process.stdin.once('data', () => {
|
||||
process.permission.drop('env');
|
||||
console.log('dropped');
|
||||
process.stdin.once('data', () => {});
|
||||
});
|
||||
`,
|
||||
],
|
||||
{ env, stdio: ['pipe', 'pipe', 'inherit'] });
|
||||
|
||||
child.stdout.once('data', common.mustCall(() => {
|
||||
child.stdout.once('data', common.mustCall(() => {
|
||||
const environ = fs.readFileSync(`/proc/${child.pid}/environ`, 'latin1');
|
||||
assert.doesNotMatch(environ, /PERMISSION_ENV_SECRET|secret-value/);
|
||||
assert.doesNotMatch(environ, /PERMISSION_ENV_ALLOWED|allowed-value/);
|
||||
child.stdin.end('done');
|
||||
}));
|
||||
|
||||
child.stdin.write('drop');
|
||||
}));
|
||||
|
||||
child.on('exit', common.mustCall((code, signal) => {
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
const assert = require('assert');
|
||||
const { spawnSyncAndAssert, spawnSyncAndExit } = require('../common/child_process');
|
||||
|
||||
const names = [
|
||||
'PERMISSION_ENV_SECRET',
|
||||
'PERMISSION_ENV_ALLOWED',
|
||||
'PERMISSION_ENV_PREFIX_ONE',
|
||||
'PERMISSION_ENV_PREFIX_TWO',
|
||||
// Starts with NODE_, but is not one of the variables Node.js reads.
|
||||
'NODE_AUTH_TOKEN',
|
||||
// Read by Node.js itself, so it is kept.
|
||||
'TZ',
|
||||
];
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PERMISSION_ENV_SECRET: 'secret',
|
||||
PERMISSION_ENV_ALLOWED: 'allowed',
|
||||
PERMISSION_ENV_PREFIX_ONE: 'one',
|
||||
PERMISSION_ENV_PREFIX_TWO: 'two',
|
||||
NODE_AUTH_TOKEN: 'token',
|
||||
TZ: 'UTC',
|
||||
};
|
||||
|
||||
const script = `
|
||||
const names = ${JSON.stringify(names)};
|
||||
const { environmentVariables } = process.report.getReport();
|
||||
console.log(JSON.stringify({
|
||||
values: Object.fromEntries(names.map((name) => [name, process.env[name]])),
|
||||
keys: names.filter((name) => Object.keys(process.env).includes(name)),
|
||||
report: names.filter((name) => name in environmentVariables),
|
||||
has: process.permission &&
|
||||
Object.fromEntries(names.map((name) => [name, process.permission.has('env', name)])),
|
||||
hasAll: process.permission?.has('env'),
|
||||
}));
|
||||
`;
|
||||
|
||||
function run(...flags) {
|
||||
let result;
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[...flags, '--no-warnings', '-e', script],
|
||||
{ env },
|
||||
{
|
||||
stdout(output) {
|
||||
result = JSON.parse(output);
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Variables --allow-env does not grant access to are removed at startup.
|
||||
{
|
||||
const result = run(
|
||||
'--permission',
|
||||
'--allow-env=PERMISSION_ENV_ALLOWED,PERMISSION_ENV_PREFIX_*');
|
||||
const visible = [
|
||||
'PERMISSION_ENV_ALLOWED',
|
||||
'PERMISSION_ENV_PREFIX_ONE',
|
||||
'PERMISSION_ENV_PREFIX_TWO',
|
||||
'TZ',
|
||||
];
|
||||
assert.deepStrictEqual(result.values, {
|
||||
PERMISSION_ENV_ALLOWED: 'allowed',
|
||||
PERMISSION_ENV_PREFIX_ONE: 'one',
|
||||
PERMISSION_ENV_PREFIX_TWO: 'two',
|
||||
TZ: 'UTC',
|
||||
});
|
||||
assert.deepStrictEqual(result.keys, visible);
|
||||
assert.deepStrictEqual(result.report, visible);
|
||||
assert.deepStrictEqual(result.has, {
|
||||
PERMISSION_ENV_SECRET: false,
|
||||
PERMISSION_ENV_ALLOWED: true,
|
||||
PERMISSION_ENV_PREFIX_ONE: true,
|
||||
PERMISSION_ENV_PREFIX_TWO: true,
|
||||
NODE_AUTH_TOKEN: false,
|
||||
TZ: true,
|
||||
});
|
||||
assert.strictEqual(result.hasAll, false);
|
||||
}
|
||||
|
||||
// --allow-env can be given more than once.
|
||||
{
|
||||
const result = run(
|
||||
'--permission',
|
||||
'--allow-env=PERMISSION_ENV_ALLOWED',
|
||||
'--allow-env=PERMISSION_ENV_PREFIX_ONE');
|
||||
assert.deepStrictEqual(result.keys, [
|
||||
'PERMISSION_ENV_ALLOWED',
|
||||
'PERMISSION_ENV_PREFIX_ONE',
|
||||
'TZ',
|
||||
]);
|
||||
}
|
||||
|
||||
// Without --allow-env, only the variables Node.js reads itself are kept.
|
||||
{
|
||||
const result = run('--permission');
|
||||
assert.deepStrictEqual(result.keys, ['TZ']);
|
||||
assert.strictEqual(result.hasAll, false);
|
||||
}
|
||||
|
||||
// --allow-env=* grants access to every variable.
|
||||
{
|
||||
const result = run('--permission', '--allow-env=*');
|
||||
assert.deepStrictEqual(result.keys, names);
|
||||
assert.deepStrictEqual(result.report, names);
|
||||
assert.strictEqual(result.hasAll, true);
|
||||
}
|
||||
|
||||
// Audit mode does not remove anything.
|
||||
{
|
||||
const result = run('--permission-audit', '--allow-env=PERMISSION_ENV_ALLOWED');
|
||||
assert.deepStrictEqual(result.keys, names);
|
||||
assert.strictEqual(result.has.PERMISSION_ENV_SECRET, false);
|
||||
assert.strictEqual(result.has.PERMISSION_ENV_ALLOWED, true);
|
||||
assert.strictEqual(result.hasAll, false);
|
||||
}
|
||||
|
||||
// --allow-env requires the permission model.
|
||||
spawnSyncAndExit(
|
||||
process.execPath,
|
||||
['--allow-env=PERMISSION_ENV_ALLOWED', '-e', ''],
|
||||
{ env },
|
||||
{
|
||||
status: 1,
|
||||
signal: null,
|
||||
stderr: /ERR_MISSING_OPTION.*--permission is required|--permission is required/,
|
||||
});
|
||||
|
||||
// Workers see the removed variables as absent, whether they share the
|
||||
// environment or copy it.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--allow-worker', '--no-warnings', '-e',
|
||||
`
|
||||
const assert = require('assert');
|
||||
const { Worker, SHARE_ENV } = require('worker_threads');
|
||||
for (const workerEnv of [SHARE_ENV, undefined]) {
|
||||
new Worker(
|
||||
'require("worker_threads").parentPort.postMessage(process.env.PERMISSION_ENV_SECRET)',
|
||||
{ eval: true, env: workerEnv },
|
||||
).on('message', (value) => assert.strictEqual(value, undefined));
|
||||
}
|
||||
`,
|
||||
],
|
||||
{ env },
|
||||
{});
|
||||
|
||||
// Environment variable names are case-insensitive on Windows.
|
||||
if (common.isWindows) {
|
||||
const result = run('--permission', '--allow-env=permission_env_allowed');
|
||||
assert.deepStrictEqual(result.keys, ['PERMISSION_ENV_ALLOWED', 'TZ']);
|
||||
assert.strictEqual(result.has.PERMISSION_ENV_ALLOWED, true);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const { isMainThread } = require('worker_threads');
|
||||
|
||||
if (!isMainThread) {
|
||||
common.skip('This test only works on a main thread');
|
||||
}
|
||||
|
||||
const assert = require('assert');
|
||||
const { spawnSyncAndAssert } = require('../common/child_process');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PERMISSION_ENV_SECRET: 'secret',
|
||||
};
|
||||
|
||||
// Reading a variable that was removed at startup publishes a denial every
|
||||
// time, and warns once.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '-e',
|
||||
`
|
||||
const assert = require('assert');
|
||||
const dc = require('diagnostics_channel');
|
||||
const events = [];
|
||||
dc.subscribe('node:permission-model:env', (message) => {
|
||||
events.push([message.permission, message.resource]);
|
||||
});
|
||||
|
||||
assert.strictEqual(process.env.PERMISSION_ENV_SECRET, undefined);
|
||||
assert.strictEqual(process.env.PERMISSION_ENV_SECRET, undefined);
|
||||
assert.strictEqual('PERMISSION_ENV_SECRET' in process.env, false);
|
||||
// Variables that were never set are not reported.
|
||||
assert.strictEqual(process.env.PERMISSION_ENV_NEVER_SET, undefined);
|
||||
|
||||
assert.deepStrictEqual(events, [
|
||||
['Env', 'PERMISSION_ENV_SECRET'],
|
||||
['Env', 'PERMISSION_ENV_SECRET'],
|
||||
['Env', 'PERMISSION_ENV_SECRET'],
|
||||
]);
|
||||
`,
|
||||
],
|
||||
{ env },
|
||||
{
|
||||
stderr(output) {
|
||||
const warnings = output.match(
|
||||
/Warning: The permission model removed the environment variable "PERMISSION_ENV_SECRET" at startup\. Use --allow-env to manage permissions\./g);
|
||||
assert.strictEqual(warnings?.length, 1, output);
|
||||
assert.doesNotMatch(output, /PERMISSION_ENV_NEVER_SET/);
|
||||
},
|
||||
});
|
||||
|
||||
// --no-warnings silences the warning.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['--permission', '--no-warnings', '-e', 'process.env.PERMISSION_ENV_SECRET'],
|
||||
{ env },
|
||||
{ stderr: '' });
|
||||
|
||||
// In audit mode nothing is removed. Accesses to variables that --allow-env
|
||||
// does not grant access to are published instead, without a warning.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission-audit', '--allow-env=PERMISSION_ENV_ALLOWED', '-e',
|
||||
`
|
||||
const assert = require('assert');
|
||||
const dc = require('diagnostics_channel');
|
||||
const events = [];
|
||||
dc.subscribe('node:permission-model:env', (message) => {
|
||||
events.push(message.resource);
|
||||
});
|
||||
|
||||
assert.strictEqual(process.env.PERMISSION_ENV_SECRET, 'secret');
|
||||
assert.strictEqual('PERMISSION_ENV_SECRET' in process.env, true);
|
||||
assert.strictEqual(process.env.PERMISSION_ENV_ALLOWED, 'allowed');
|
||||
process.env.TZ;
|
||||
// Variables created at runtime would not have been removed either.
|
||||
process.env.PERMISSION_ENV_RUNTIME = 'runtime';
|
||||
assert.strictEqual(process.env.PERMISSION_ENV_RUNTIME, 'runtime');
|
||||
|
||||
assert.deepStrictEqual(events, [
|
||||
'PERMISSION_ENV_SECRET',
|
||||
'PERMISSION_ENV_SECRET',
|
||||
]);
|
||||
`,
|
||||
],
|
||||
{ env: { ...env, PERMISSION_ENV_ALLOWED: 'allowed' } },
|
||||
{
|
||||
stderr(output) {
|
||||
assert.doesNotMatch(output, /removed the environment variable/);
|
||||
},
|
||||
});
|
||||
|
||||
// --use-env-proxy stops applying the proxy variables the permission model
|
||||
// removes, which a single warning points out. Reading them afterwards does
|
||||
// not warn again.
|
||||
{
|
||||
const proxyEnv = { ...process.env };
|
||||
for (const name of ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY']) {
|
||||
delete proxyEnv[name];
|
||||
delete proxyEnv[name.toLowerCase()];
|
||||
}
|
||||
delete proxyEnv.NODE_USE_ENV_PROXY;
|
||||
proxyEnv.HTTP_PROXY = 'http://proxy.invalid:8080';
|
||||
proxyEnv.NO_PROXY = 'localhost';
|
||||
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--use-env-proxy', '-e',
|
||||
'process.env.HTTP_PROXY; process.env.NO_PROXY;',
|
||||
],
|
||||
{ env: proxyEnv },
|
||||
{
|
||||
stderr(output) {
|
||||
const warnings = output.match(
|
||||
/Warning: --use-env-proxy is enabled, but the permission model removed HTTP_PROXY, NO_PROXY from the environment at startup/g);
|
||||
assert.strictEqual(warnings?.length, 1, output);
|
||||
assert.doesNotMatch(output, /removed the environment variable/);
|
||||
},
|
||||
});
|
||||
|
||||
// NODE_USE_ENV_PROXY enables it too.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['--permission', '-e', '0'],
|
||||
{ env: { ...proxyEnv, NODE_USE_ENV_PROXY: '1' } },
|
||||
{ stderr: /--use-env-proxy is enabled, but the permission model removed HTTP_PROXY, NO_PROXY/ });
|
||||
|
||||
// Only the variables that were removed are named.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['--permission', '--use-env-proxy', '--allow-env=HTTP_PROXY', '-e', '0'],
|
||||
{ env: proxyEnv },
|
||||
{
|
||||
stderr(output) {
|
||||
assert.match(output, /the permission model removed NO_PROXY from/);
|
||||
},
|
||||
});
|
||||
|
||||
// No warning once they are allowed.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission', '--use-env-proxy', '--allow-env=HTTP_PROXY,NO_PROXY',
|
||||
'-e', '0',
|
||||
],
|
||||
{ env: proxyEnv },
|
||||
{ stderr: '' });
|
||||
|
||||
// Nor without --use-env-proxy, which does not read them.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['--permission', '-e', '0'],
|
||||
{ env: proxyEnv },
|
||||
{ stderr: '' });
|
||||
|
||||
// Nor in audit mode, which removes nothing.
|
||||
spawnSyncAndAssert(
|
||||
process.execPath,
|
||||
['--permission-audit', '--use-env-proxy', '-e', '0'],
|
||||
{ env: proxyEnv },
|
||||
{ stderr: '' });
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Flags: --permission --allow-fs-read=* --allow-fs-write=* --allow-child-process
|
||||
// Flags: --permission --allow-fs-read=* --allow-fs-write=* --allow-child-process --allow-env=NODE_TEST_DIR,TEST_*
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
@@ -43,6 +43,7 @@ const commonPath = path.join(__filename, '../../common');
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-env=BOUNDARY_FILE',
|
||||
...grantedFiles.map((file) => `--allow-fs-read=${file}`),
|
||||
...grantedFiles.map((file) => `--allow-fs-write=${file}`),
|
||||
'-e',
|
||||
@@ -79,6 +80,7 @@ const commonPath = path.join(__filename, '../../common');
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-env=BLOCKEDFILE,BLOCKEDFOLDER,ALLOWEDFOLDER',
|
||||
// Do not uncomment this line
|
||||
// `--allow-fs-read=${file}`,
|
||||
`--allow-fs-read=${commonPathWildcard}`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Flags: --permission --allow-fs-read=* --allow-fs-write=* --allow-child-process
|
||||
// Flags: --permission --allow-fs-read=* --allow-fs-write=* --allow-child-process --allow-env=NODE_TEST_DIR,TEST_*
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
@@ -42,6 +42,7 @@ fs.writeFileSync(path.join(readWriteFolder, 'file'), 'NO evil file contents');
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-env=READONLYFOLDER,READWRITEFOLDER,WRITEONLYFOLDER',
|
||||
`--allow-fs-read=${file}`, `--allow-fs-read=${commonPathWildcard}`, `--allow-fs-read=${readOnlyFolder}`, `--allow-fs-read=${readWriteFolder}`,
|
||||
`--allow-fs-write=${readWriteFolder}`, `--allow-fs-write=${writeOnlyFolder}`,
|
||||
file,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Flags: --permission --allow-fs-read=* --allow-fs-write=* --allow-child-process
|
||||
// Flags: --permission --allow-fs-read=* --allow-fs-write=* --allow-child-process --allow-env=NODE_TEST_DIR,TEST_*
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
@@ -54,6 +54,7 @@ const traversalSymlink = path.join(allowedFolder, 'deep1', 'deep2', 'deep3', 'go
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-env=BLOCKEDFILE,BLOCKEDFOLDER,EXISTINGSYMLINK',
|
||||
`--allow-fs-read=${file}`, `--allow-fs-read=${commonPathWildcard}`, `--allow-fs-read=${symlinkFromBlockedFile}`,
|
||||
`--allow-fs-read=${allowedFolder}`,
|
||||
`--allow-fs-write=${symlinkFromBlockedFile}`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Flags: --permission --allow-fs-read=* --allow-fs-write=* --allow-child-process
|
||||
// Flags: --permission --allow-fs-read=* --allow-fs-write=* --allow-child-process --allow-env=NODE_TEST_DIR,TEST_*
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
@@ -38,6 +38,7 @@ const commonPathWildcard = path.join(__filename, '../../common*');
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-env=ALLOWEDFOLDER,BLOCKEDFOLDER',
|
||||
`--allow-fs-read=${file}`, `--allow-fs-read=${commonPathWildcard}`, `--allow-fs-read=${allowedFolder}`,
|
||||
`--allow-fs-write=${allowedFolder}`,
|
||||
file,
|
||||
|
||||
@@ -15,7 +15,7 @@ if (!common.hasCrypto) {
|
||||
// We need to define the flags dynamically to account for the `NODE_TEST_DIR` env var.
|
||||
if (!process.permission) {
|
||||
spawnSyncAndExitWithoutError(process.execPath, [
|
||||
'--permission',
|
||||
'--permission', '--allow-env=NODE_TEST_DIR,TEST_*',
|
||||
'--allow-fs-read=*', `--allow-fs-write=${process.env.NODE_TEST_DIR || './test'}/.tmp.*`, '--allow-child-process',
|
||||
__filename,
|
||||
]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Flags: --permission --allow-fs-read=* --allow-child-process
|
||||
// Flags: --permission --allow-fs-read=* --allow-child-process --allow-env=NODE_TEST_DIR,TEST_*
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
@@ -31,6 +31,7 @@ const file = fixtures.path('permission', 'fs-write.js');
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-env=ALLOWEDFILE,ALLOWEDFOLDER,BLOCKEDFILE,BLOCKEDFOLDER,RELATIVEBLOCKEDFILE,RELATIVEBLOCKEDFOLDER',
|
||||
'--allow-fs-read=*',
|
||||
`--allow-fs-write=${regularFile}`, `--allow-fs-write=${commonPath}`,
|
||||
file,
|
||||
|
||||
@@ -32,6 +32,7 @@ const { status, stderr } = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-env=ALLOWED_DIR,ALLOWED_FILE,DENIED_FILE',
|
||||
`--allow-fs-read=${allowedDir}`,
|
||||
fixtures.path('permission', 'has-reference-types.js'),
|
||||
],
|
||||
|
||||
@@ -25,6 +25,7 @@ const server = http.createServer((req, res) => {
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-env=URL',
|
||||
'--allow-fs-read=*',
|
||||
file,
|
||||
],
|
||||
|
||||
@@ -21,6 +21,7 @@ const server = net.createServer().listen(0, common.mustCall(() => {
|
||||
process.execPath,
|
||||
[
|
||||
'--permission',
|
||||
'--allow-env=HOST,PORT',
|
||||
'--allow-fs-read=*',
|
||||
file,
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Flags: --permission --allow-net --allow-fs-read=*
|
||||
// Flags: --permission --allow-net --allow-fs-read=* --allow-env=*
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Flags: --permission --allow-fs-read=* --allow-fs-write=* --allow-openssl-store --allow-child-process
|
||||
// Flags: --permission --allow-fs-read=* --allow-fs-write=* --allow-openssl-store --allow-child-process --allow-env=NODE_TEST_DIR,TEST_*
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
|
||||
Reference in new issue
Block a user