Skip to content

Commit 3b515c2

Browse files
committed
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
1 parent 03fcd8b commit 3b515c2

46 files changed

Lines changed: 2546 additions & 23 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎doc/api/cli.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,51 @@ This behavior also applies to `child_process.spawn()`, but in that case, the
191191
flags are propagated via the `NODE_OPTIONS` environment variable rather than
192192
directly through the process arguments.
193193

194+
### `--allow-env`
195+
196+
<!-- YAML
197+
added: REPLACEME
198+
-->
199+
200+
> Stability: 1.1 - Active development
201+
202+
When using the [Permission Model][], the process starts without the environment
203+
variables it has not been granted access to. At startup, every variable that
204+
`--allow-env` does not match is removed from the process environment. Removed
205+
variables are absent from `process.env`, from diagnostic reports, from native
206+
code calling `getenv()`, and from the environment of child processes and worker
207+
threads.
208+
209+
The valid values are:
210+
211+
* `*` - Grants access to every environment variable.
212+
* A variable name, for example `--allow-env=DATABASE_URL`.
213+
* A variable name prefix followed by `*`, for example `--allow-env=APP_*`.
214+
215+
Multiple values can be passed by repeating the flag, or by separating them with
216+
commas: `--allow-env=PORT,APP_*`. Variable names are case-insensitive on
217+
Windows.
218+
219+
Example:
220+
221+
```js
222+
console.log(process.env.DATABASE_URL);
223+
console.log(process.env.AWS_SECRET_ACCESS_KEY);
224+
```
225+
226+
```console
227+
$ node --permission --allow-fs-read=* --allow-env=DATABASE_URL index.js
228+
postgres://localhost/app
229+
undefined
230+
(node:1234) Warning: The permission model removed the environment variable "AWS_SECRET_ACCESS_KEY" at startup. Use --allow-env to manage permissions.
231+
```
232+
233+
The variables that Node.js and its bundled dependencies read, such as
234+
`NODE_OPTIONS`, `PATH`, `HOME`, `TZ`, and `SSL_CERT_FILE`, are always kept, as
235+
are the variables defined in [`--env-file`][] files. `NODE_ENV` is not kept
236+
by default, so applications and libraries that read it need
237+
`--allow-env=NODE_ENV`. See [Environment variable permissions][] for details.
238+
194239
### `--allow-ffi`
195240

196241
<!-- YAML
@@ -2538,6 +2583,7 @@ following permissions are restricted:
25382583
* File System - manageable through
25392584
[`--allow-fs-read`][], [`--allow-fs-write`][] flags
25402585
* Network - manageable through [`--allow-net`][] flag
2586+
* Environment variables - manageable through [`--allow-env`][] flag
25412587
* Child Process - manageable through [`--allow-child-process`][] flag
25422588
* Worker Threads - manageable through [`--allow-worker`][] flag
25432589
* WASI - manageable through [`--allow-wasi`][] flag
@@ -4128,6 +4174,7 @@ one is included in the list below.
41284174

41294175
* `--allow-addons`
41304176
* `--allow-child-process`
4177+
* `--allow-env`
41314178
* `--allow-ffi`
41324179
* `--allow-fs-read`
41334180
* `--allow-fs-vfs`
@@ -4776,6 +4823,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
47764823
[CommonJS module]: modules.md
47774824
[DEP0025 warning]: deprecations.md#dep0025-requirenodesys
47784825
[ECMAScript module]: esm.md#modules-ecmascript-modules
4826+
[Environment variable permissions]: permissions.md#environment-variable-permissions
47794827
[EventSource Web API]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
47804828
[ExperimentalWarning: `vm.measureMemory` is an experimental feature]: vm.md#vmmeasurememoryoptions
47814829
[FIPS mode]: crypto.md#fips-mode
@@ -4799,6 +4847,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
47994847
[`'crypto.fips.indicator'`]: diagnostics_channel.md#event-cryptofipsindicator
48004848
[`--allow-addons`]: #--allow-addons
48014849
[`--allow-child-process`]: #--allow-child-process
4850+
[`--allow-env`]: #--allow-env
48024851
[`--allow-fs-read`]: #--allow-fs-read
48034852
[`--allow-fs-write`]: #--allow-fs-write
48044853
[`--allow-net`]: #--allow-net

‎doc/api/embedding.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,51 @@ int main(int argc, char** argv) {
7272
}
7373
```
7474
75+
### Restricting access to environment variables
76+
77+
<!-- YAML
78+
added: REPLACEME
79+
-->
80+
81+
When the arguments passed to `node::InitializeOncePerProcess()` enable the
82+
[Permission Model][] without `--allow-env=*`, the process environment must not
83+
contain any variable that [`--allow-env`][] does not grant access to.
84+
`node::InitializeOncePerProcess()` fails otherwise. Unlike the `node`
85+
executable, embedders own the process environment, so Node.js does not remove
86+
these variables itself.
87+
88+
`node::ScrubProcessEnvironment()` removes them. Because it modifies the process
89+
environment without any locking that native code calling `getenv()`
90+
participates in, it must be called before starting any thread that may read the
91+
environment, and before `node::InitializeOncePerProcess()`:
92+
93+
```cpp
94+
int main(int argc, char** argv) {
95+
argv = uv_setup_args(argc, argv);
96+
std::vector<std::string> args(argv, argv + argc);
97+
98+
// Keep the variables the embedder itself reads, in addition to the ones
99+
// Node.js reads (see node::GetRuntimeEnvironmentDefaults()).
100+
node::ProcessEnvironmentScrubOptions scrub_options;
101+
scrub_options.allow = {"PORT", "APP_*"};
102+
if (node::ScrubProcessEnvironment(scrub_options).IsNothing()) {
103+
return 1;
104+
}
105+
106+
// args contains, for example, --permission --allow-env=PORT
107+
std::unique_ptr<node::InitializationResult> result =
108+
node::InitializeOncePerProcess(args, {
109+
node::ProcessInitializationFlags::kNoInitializeV8,
110+
node::ProcessInitializationFlags::kNoInitializeNodeV8Platform
111+
});
112+
// ...
113+
}
114+
```
115+
116+
`process.permission.drop('env', name)` removes a variable from the process
117+
environment, so it throws when called from a `node::Environment` created
118+
without `node::EnvironmentFlags::kOwnsProcessState`.
119+
75120
### Setting up a per-instance state
76121

77122
<!-- YAML
@@ -178,6 +223,8 @@ int RunNodeInstance(MultiIsolatePlatform* platform,
178223
```
179224
180225
[CLI options]: cli.md
226+
[Permission Model]: permissions.md#permission-model
227+
[`--allow-env`]: cli.md#--allow-env
181228
[`process.memoryUsage()`]: process.md#processmemoryusage
182229
[deprecation policy]: deprecations.md
183230
[embedtest.cc]: https://github.com/nodejs/node/blob/HEAD/test/embedding/embedtest.cc

‎doc/api/permissions.md‎

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ The Permission Model has two operational modes:
6161

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

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

82+
To grant access to environment variables, use [`--allow-env`][].
83+
8284
To allow network access, use [`--allow-net`][] and for allowing native addons
8385
when using permission model, use the [`--allow-addons`][]
8486
flag. For WASI, use the [`--allow-wasi`][] flag. For FFI, use the
@@ -157,9 +159,9 @@ mode. Execution continues normally.
157159
Audit mode is useful for discovering what permissions your application
158160
requires before deploying with [`--permission`][]. It can also be combined
159161
with the [`--allow-fs-read`][], [`--allow-fs-write`][], [`--allow-net`][],
160-
[`--allow-child-process`][], [`--allow-worker`][], [`--allow-addons`][],
161-
[`--allow-wasi`][], and [`--allow-ffi`][] flags to audit a subset of
162-
permissions while granting others.
162+
[`--allow-env`][], [`--allow-child-process`][], [`--allow-worker`][],
163+
[`--allow-addons`][], [`--allow-wasi`][], and [`--allow-ffi`][] flags to audit
164+
a subset of permissions while granting others.
163165

164166
When a permission check fails in audit mode, a message is published to the
165167
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:
172174
* `node:permission-model:wasi` — WASI
173175
* `node:permission-model:addon` — Native Addons
174176
* `node:permission-model:ffi` — FFI
177+
* `node:permission-model:env` — Environment variables
175178

176179
Each message is an object with the following properties:
177180

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

272+
#### Environment variable permissions
273+
274+
When the Permission Model is enforced, the process only has access to the
275+
environment variables that [`--allow-env`][] grants access to.
276+
277+
Instead of checking each access, Node.js removes every other variable from the
278+
process environment at startup, before any JavaScript code runs and before
279+
Node.js starts any other thread. Removed variables are absent from everything
280+
that exposes the environment of the process: `process.env`, diagnostic reports,
281+
native code calling `getenv()`, worker threads, and the environment inherited by
282+
child processes.
283+
284+
```console
285+
$ node --permission --allow-env=PORT --allow-env=APP_* index.js
286+
```
287+
288+
The valid arguments for the flag are:
289+
290+
* `*` - Grants access to every environment variable. Nothing is removed.
291+
* A variable name, such as `PORT`.
292+
* A variable name prefix followed by `*`, such as `APP_*`.
293+
294+
Some variables are always kept:
295+
296+
* The variables that Node.js and its bundled dependencies read after startup,
297+
such as `NODE_OPTIONS`, `NODE_EXTRA_CA_CERTS`, `PATH`, `HOME`, `TMPDIR`, `TZ`,
298+
`LANG`, `SSL_CERT_FILE`, and the variables that terminal color detection
299+
reads. Other variables whose names start with `NODE_`, such as
300+
`NODE_AUTH_TOKEN`, are not kept.
301+
* The variables defined in the files passed to [`--env-file`][] and
302+
[`--env-file-if-exists`][]. If a variable is defined in such a file and also
303+
inherited from the parent process, and `--allow-env` does not grant access to
304+
it, the inherited value is removed and the value from the file is used.
305+
306+
`NODE_ENV` is not kept either. Node.js does not read it, but many applications
307+
and libraries do, and treat it being unset as a development environment. Grant
308+
access to it explicitly:
309+
310+
```console
311+
$ node --permission --allow-env=NODE_ENV index.js
312+
```
313+
314+
Proxy URLs often contain credentials, so the `HTTP_PROXY`, `HTTPS_PROXY`, and
315+
`NO_PROXY` variables, and their lowercase forms, are not kept. Grant access to
316+
them explicitly when using [`--use-env-proxy`][]. When `--use-env-proxy` is
317+
enabled and any of them were removed at startup, a warning naming them is
318+
emitted.
319+
320+
Reading a variable that was removed at startup returns `undefined`, emits a
321+
warning the first time, and publishes a message to the
322+
`node:permission-model:env` diagnostics channel.
323+
324+
Variables set at runtime, for example with `process.env.KEY = 'value'` or
325+
[`process.loadEnvFile()`][], are not restricted, as they cannot reveal what was
326+
removed.
327+
328+
Dropping a variable with [`permission.drop()`][] removes it from the
329+
environment. Dropping the whole `env` scope removes every variable except the
330+
ones Node.js reads itself. This makes it possible to read a secret during
331+
initialization, and then remove it:
332+
333+
```js
334+
const databaseUrl = process.env.DATABASE_URL;
335+
process.permission.drop('env', 'DATABASE_URL');
336+
```
337+
338+
When a process that enforces the Permission Model spawns a child process, the
339+
child is started with `--allow-env=*`: the environment it inherits only contains
340+
variables that the parent had access to. The child can still read its own
341+
`/proc/<pid>/environ` on Linux, but not that of any other process, see below.
342+
343+
In audit mode, nothing is removed. Accesses to variables that `--allow-env`
344+
does not grant access to are published to the `node:permission-model:env`
345+
diagnostics channel instead.
346+
347+
On Linux, `/proc/<pid>/environ` exposes the environment a process was started
348+
with. When the Permission Model is enforced, reading the `/proc/<pid>/environ`
349+
file of any other process, including the parent process and its ancestors, is
350+
denied regardless of [`--allow-fs-read`][]. Reading the process's own file is
351+
only allowed with `--allow-env=*`. Symbolic links are resolved before the
352+
check, so paths that reach these files indirectly, such as
353+
`/dev/fd/../environ`, are denied as well.
354+
355+
In addition, the removed variables are overwritten in the initial environment
356+
block of the process, so that other processes do not find them in its
357+
`/proc/<pid>/environ` either. Variables removed later with
358+
[`permission.drop()`][] are overwritten there as well.
359+
360+
These measures do not change the environment of other processes. A process
361+
granted [`--allow-child-process`][] can read their environment through other
362+
programs.
363+
269364
#### Configuration file support
270365

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

395+
A configuration file, like the `NODE_OPTIONS` defined in an [`--env-file`][]
396+
file, may be controlled by the project being run rather than by whoever starts
397+
Node.js. When the command line or the `NODE_OPTIONS` environment variable
398+
enable the Permission Model, the `allow-env` values these files define can only
399+
narrow the access that [`--allow-env`][] grants, and never widen it:
400+
401+
```console
402+
$ node --permission --allow-env=APP_* --experimental-config-file=node.config.json app.js
403+
```
404+
405+
With `"allow-env": ["*"]` in `node.config.json`, only the variables starting with
406+
`APP_` are kept. With `"allow-env": ["APP_DATABASE_URL", "OTHER"]`, only
407+
`APP_DATABASE_URL` is.
408+
300409
#### Using the Permission Model with `npx`
301410

302411
If you're using [`npx`][] to execute a Node.js script, you can enable the
@@ -348,6 +457,7 @@ There are constraints you need to know before using this system:
348457
* When using the Permission Model the following features will be restricted:
349458
* Native modules
350459
* Network
460+
* Environment variables
351461
* Child process
352462
* Worker Threads
353463
* Inspector protocol
@@ -410,15 +520,21 @@ Developers relying on --permission to sandbox untrusted code should be aware tha
410520
[Security Policy]: https://github.com/nodejs/node/blob/main/SECURITY.md
411521
[`--allow-addons`]: cli.md#--allow-addons
412522
[`--allow-child-process`]: cli.md#--allow-child-process
523+
[`--allow-env`]: cli.md#--allow-env
413524
[`--allow-ffi`]: cli.md#--allow-ffi
414525
[`--allow-fs-read`]: cli.md#--allow-fs-read
415526
[`--allow-fs-write`]: cli.md#--allow-fs-write
416527
[`--allow-net`]: cli.md#--allow-net
417528
[`--allow-openssl-store`]: cli.md#--allow-openssl-store
418529
[`--allow-wasi`]: cli.md#--allow-wasi
419530
[`--allow-worker`]: cli.md#--allow-worker
531+
[`--env-file-if-exists`]: cli.md#--env-file-if-existsfile
532+
[`--env-file`]: cli.md#--env-filefile
420533
[`--permission-audit`]: cli.md#--permission-audit
421534
[`--permission`]: cli.md#--permission
535+
[`--use-env-proxy`]: cli.md#--use-env-proxy
422536
[`crypto.createPrivateKey()`]: crypto.md#cryptocreateprivatekeykey
423537
[`npx`]: https://docs.npmjs.com/cli/commands/npx
538+
[`permission.drop()`]: process.md#processpermissiondropscope-reference
424539
[`permission.has()`]: process.md#processpermissionhasscope-reference
540+
[`process.loadEnvFile()`]: process.md#processloadenvfilepath

‎doc/api/process.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3163,6 +3163,7 @@ The available scopes are:
31633163
* `fs.read` - File System read operations
31643164
* `fs.write` - File System write operations
31653165
* `child` - Child process spawning operations
3166+
* `env` - Environment variables
31663167
* `openssl.store` - Loading keys through OpenSSL STORE loaders
31673168
* `worker` - Worker thread spawning operation
31683169
* `ffi` - Foreign function interface operations
@@ -3220,6 +3221,8 @@ The available scopes are the same as [`process.permission.has()`][]:
32203221
* `fs.read` - File System read operations
32213222
* `fs.write` - File System write operations
32223223
* `child` - Child process spawning operations
3224+
* `env` - Environment variables. Dropping a variable removes it from the
3225+
environment
32233226
* `openssl.store` - Loading keys through OpenSSL STORE loaders
32243227
* `worker` - Worker thread spawning operation
32253228
* `net` - Network operations

0 commit comments

Comments
 (0)