Skip to content

Commit 066fcbd

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, patterns (`PREFI_*`), 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. 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 and fs reads of /proc/*/environ are denied. 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, it should never be more than what the parent can see. 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 3cd2d6e commit 066fcbd

45 files changed

Lines changed: 2107 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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,50 @@ 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. See
236+
[Environment variable permissions][] for details.
237+
194238
### `--allow-ffi`
195239

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

41294174
* `--allow-addons`
41304175
* `--allow-child-process`
4176+
* `--allow-env`
41314177
* `--allow-ffi`
41324178
* `--allow-fs-read`
41334179
* `--allow-fs-vfs`
@@ -4776,6 +4822,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
47764822
[CommonJS module]: modules.md
47774823
[DEP0025 warning]: deprecations.md#dep0025-requirenodesys
47784824
[ECMAScript module]: esm.md#modules-ecmascript-modules
4825+
[Environment variable permissions]: permissions.md#environment-variable-permissions
47794826
[EventSource Web API]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
47804827
[ExperimentalWarning: `vm.measureMemory` is an experimental feature]: vm.md#vmmeasurememoryoptions
47814828
[FIPS mode]: crypto.md#fips-mode
@@ -4799,6 +4846,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
47994846
[`'crypto.fips.indicator'`]: diagnostics_channel.md#event-cryptofipsindicator
48004847
[`--allow-addons`]: #--allow-addons
48014848
[`--allow-child-process`]: #--allow-child-process
4849+
[`--allow-env`]: #--allow-env
48024850
[`--allow-fs-read`]: #--allow-fs-read
48034851
[`--allow-fs-write`]: #--allow-fs-write
48044852
[`--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: 102 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,78 @@ 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+
Proxy URLs often contain credentials, so the `HTTP_PROXY`, `HTTPS_PROXY`, and
307+
`NO_PROXY` variables are not kept. Grant access to them explicitly when using
308+
[`--use-env-proxy`][].
309+
310+
Reading a variable that was removed at startup returns `undefined`, emits a
311+
warning the first time, and publishes a message to the
312+
`node:permission-model:env` diagnostics channel.
313+
314+
Variables set at runtime, for example with `process.env.KEY = 'value'` or
315+
[`process.loadEnvFile()`][], are not restricted, as they cannot reveal what was
316+
removed.
317+
318+
Dropping a variable with [`permission.drop()`][] removes it from the
319+
environment. Dropping the whole `env` scope removes every variable except the
320+
ones Node.js reads itself. This makes it possible to read a secret during
321+
initialization, and then remove it:
322+
323+
```js
324+
const databaseUrl = process.env.DATABASE_URL;
325+
process.permission.drop('env', 'DATABASE_URL');
326+
```
327+
328+
When a process that enforces the Permission Model spawns a child process, the
329+
child is started with `--allow-env=*`: the environment it inherits only contains
330+
variables that the parent had access to.
331+
332+
In audit mode, nothing is removed. Accesses to variables that `--allow-env`
333+
does not grant access to are published to the `node:permission-model:env`
334+
diagnostics channel instead.
335+
336+
On Linux, `/proc/<pid>/environ` exposes the environment a process was started
337+
with. While access to environment variables is restricted, reading any
338+
`/proc/<pid>/environ` file is denied, regardless of [`--allow-fs-read`][], and
339+
the removed variables are overwritten in the initial environment block of the
340+
process. This does not affect the environment of other processes, such as the
341+
parent process. A process granted [`--allow-child-process`][] can read their
342+
environment through other programs.
343+
269344
#### Configuration file support
270345

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

375+
A configuration file, like the `NODE_OPTIONS` defined in an [`--env-file`][]
376+
file, may be controlled by the project being run rather than by whoever starts
377+
Node.js. When the command line or the `NODE_OPTIONS` environment variable
378+
enable the Permission Model, the `allow-env` values these files define can only
379+
narrow the access that [`--allow-env`][] grants, and never widen it:
380+
381+
```console
382+
$ node --permission --allow-env=APP_* --experimental-config-file=node.config.json app.js
383+
```
384+
385+
With `"allow-env": ["*"]` in `node.config.json`, only the variables starting with
386+
`APP_` are kept. With `"allow-env": ["APP_DATABASE_URL", "OTHER"]`, only
387+
`APP_DATABASE_URL` is.
388+
300389
#### Using the Permission Model with `npx`
301390

302391
If you're using [`npx`][] to execute a Node.js script, you can enable the
@@ -342,6 +431,7 @@ There are constraints you need to know before using this system:
342431
* When using the Permission Model the following features will be restricted:
343432
* Native modules
344433
* Network
434+
* Environment variables
345435
* Child process
346436
* Worker Threads
347437
* Inspector protocol
@@ -404,15 +494,21 @@ Developers relying on --permission to sandbox untrusted code should be aware tha
404494
[Security Policy]: https://github.com/nodejs/node/blob/main/SECURITY.md
405495
[`--allow-addons`]: cli.md#--allow-addons
406496
[`--allow-child-process`]: cli.md#--allow-child-process
497+
[`--allow-env`]: cli.md#--allow-env
407498
[`--allow-ffi`]: cli.md#--allow-ffi
408499
[`--allow-fs-read`]: cli.md#--allow-fs-read
409500
[`--allow-fs-write`]: cli.md#--allow-fs-write
410501
[`--allow-net`]: cli.md#--allow-net
411502
[`--allow-openssl-store`]: cli.md#--allow-openssl-store
412503
[`--allow-wasi`]: cli.md#--allow-wasi
413504
[`--allow-worker`]: cli.md#--allow-worker
505+
[`--env-file-if-exists`]: cli.md#--env-file-if-existsfile
506+
[`--env-file`]: cli.md#--env-filefile
414507
[`--permission-audit`]: cli.md#--permission-audit
415508
[`--permission`]: cli.md#--permission
509+
[`--use-env-proxy`]: cli.md#--use-env-proxy
416510
[`crypto.createPrivateKey()`]: crypto.md#cryptocreateprivatekeykey
417511
[`npx`]: https://docs.npmjs.com/cli/commands/npx
512+
[`permission.drop()`]: process.md#processpermissiondropscope-reference
418513
[`permission.has()`]: process.md#processpermissionhasscope-reference
514+
[`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

‎doc/node.1‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,41 @@ This behavior also applies to \fBchild_process.spawn()\fR, but in that case, the
118118
flags are propagated via the \fBNODE_OPTIONS\fR environment variable rather than
119119
directly through the process arguments.
120120
.
121+
.It Fl -allow-env
122+
When using the Permission Model, the process starts without the environment
123+
variables it has not been granted access to. At startup, every variable that
124+
\fB--allow-env\fR does not match is removed from the process environment. Removed
125+
variables are absent from \fBprocess.env\fR, from diagnostic reports, from native
126+
code calling \fBgetenv()\fR, and from the environment of child processes and worker
127+
threads.
128+
The valid values are:
129+
.Bl -bullet
130+
.It
131+
\fB*\fR - Grants access to every environment variable.
132+
.It
133+
A variable name, for example \fB--allow-env=DATABASE_URL\fR.
134+
.It
135+
A variable name prefix followed by \fB*\fR, for example \fB--allow-env=APP_*\fR.
136+
.El
137+
Multiple values can be passed by repeating the flag, or by separating them with
138+
commas: \fB--allow-env=PORT,APP_*\fR. Variable names are case-insensitive on
139+
Windows.
140+
Example:
141+
.Bd -literal
142+
console.log(process.env.DATABASE_URL);
143+
console.log(process.env.AWS_SECRET_ACCESS_KEY);
144+
.Ed
145+
.Bd -literal
146+
$ node --permission --allow-fs-read=* --allow-env=DATABASE_URL index.js
147+
postgres://localhost/app
148+
undefined
149+
(node:1234) Warning: The permission model removed the environment variable "AWS_SECRET_ACCESS_KEY" at startup. Use --allow-env to manage permissions.
150+
.Ed
151+
The variables that Node.js and its bundled dependencies read, such as
152+
\fBNODE_OPTIONS\fR, \fBPATH\fR, \fBHOME\fR, \fBTZ\fR, and \fBSSL_CERT_FILE\fR, are always kept, as
153+
are the variables defined in \fB--env-file\fR files. See
154+
Environment variable permissions for details.
155+
.
121156
.It Fl -allow-ffi
122157
When using the Permission Model, the process will not be able to use FFI
123158
APIs by default. Attempts to use FFI APIs will throw an \fBERR_ACCESS_DENIED\fR
@@ -1274,6 +1309,8 @@ File System - manageable through
12741309
.It
12751310
Network - manageable through \fB--allow-net\fR flag
12761311
.It
1312+
Environment variables - manageable through \fB--allow-env\fR flag
1313+
.It
12771314
Child Process - manageable through \fB--allow-child-process\fR flag
12781315
.It
12791316
Worker Threads - manageable through \fB--allow-worker\fR flag
@@ -2088,6 +2125,8 @@ one is included in the list below.
20882125
.It
20892126
\fB--allow-child-process\fR
20902127
.It
2128+
\fB--allow-env\fR
2129+
.It
20912130
\fB--allow-ffi\fR
20922131
.It
20932132
\fB--allow-fs-read\fR

0 commit comments

Comments
 (0)