Skip to content

Commit 7830f6e

Browse files
committed
process: add allowedNodeEnvironmentFlags property
`process.allowedNodeEnvironmentFlags` provides an API to validate and list flags as specified in `NODE_OPTIONS` from user code. Refs: #17740 Signed-off-by: Christopher Hiller <boneskull@boneskull.com>
1 parent 9acf4c5 commit 7830f6e

File tree

7 files changed

+299
-1
lines changed

7 files changed

+299
-1
lines changed

doc/api/process.md

+51
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,55 @@ generate a core file.
416416

417417
This feature is not available in [`Worker`][] threads.
418418

419+
## process.allowedNodeEnvironmentFlags
420+
<!-- YAML
421+
added: REPLACEME
422+
-->
423+
424+
* {Set}
425+
426+
The `process.allowedNodeEnvironmentFlags` property is a special,
427+
read-only `Set` of flags allowable within the [`NODE_OPTIONS`][]
428+
environment variable.
429+
430+
`process.allowedNodeEnvironmentFlags` extends `Set`, but overrides
431+
`Set.prototype.has` to recognize several different possible flag
432+
representations. `process.allowedNodeEnvironmentFlags.has()` will
433+
return `true` in the following cases:
434+
435+
- Flags may omit leading single (`-`) or double (`--`) dashes; e.g.,
436+
`inspect-brk` for `--inspect-brk`, or `r` for `-r`.
437+
- Flags passed through to V8 (as listed in `--v8-options`) may replace
438+
one or more *non-leading* dashes for an underscore, or vice-versa;
439+
e.g., `--perf_basic_prof`, `--perf-basic-prof`, `--perf_basic-prof`,
440+
etc.
441+
- Flags may contain one or more equals (`=`) characters; all
442+
characters after and including the first equals will be ignored;
443+
e.g., `--stack-trace-limit=100`.
444+
- Flags *must* be allowable within [`NODE_OPTIONS`][].
445+
446+
When iterating over `process.allowedNodeEnvironmentFlags`, flags will
447+
appear only *once*; each will begin with one or more dashes. Flags
448+
passed through to V8 will contain underscores instead of non-leading
449+
dashes:
450+
451+
```js
452+
process.allowedNodeEnvironmentFlags.forEach((flag) => {
453+
// -r
454+
// --inspect-brk
455+
// --abort_on_uncaught_exception
456+
// ...
457+
});
458+
```
459+
460+
The methods `add()`, `clear()`, and `delete()` of
461+
`process.allowedNodeEnvironmentFlags` do nothing, and will fail
462+
silently.
463+
464+
If Node.js was compiled *without* [`NODE_OPTIONS`][] support (shown in
465+
[`process.config`][]), `process.allowedNodeEnvironmentFlags` will
466+
contain what *would have* been allowable.
467+
419468
## process.arch
420469
<!-- YAML
421470
added: v0.5.0
@@ -2061,8 +2110,10 @@ cases:
20612110
[`end()`]: stream.html#stream_writable_end_chunk_encoding_callback
20622111
[`net.Server`]: net.html#net_class_net_server
20632112
[`net.Socket`]: net.html#net_class_net_socket
2113+
[`NODE_OPTIONS`]: cli.html#cli_node_options_options
20642114
[`os.constants.dlopen`]: os.html#os_dlopen_constants
20652115
[`process.argv`]: #process_process_argv
2116+
[`process.config`]: #process_process_config
20662117
[`process.execPath`]: #process_process_execpath
20672118
[`process.exit()`]: #process_process_exit_code
20682119
[`process.exitCode`]: #process_process_exitcode

lib/internal/bootstrap/node.js

+89
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@
185185

186186
perf.markMilestone(NODE_PERFORMANCE_MILESTONE_BOOTSTRAP_COMPLETE);
187187

188+
setupAllowedFlags();
189+
188190
// There are various modes that Node can run in. The most common two
189191
// are running from a script and running the REPL - but there are a few
190192
// others like the debugger or running --eval arguments. Here we decide
@@ -631,5 +633,92 @@
631633
new vm.Script(source, { displayErrors: true, filename });
632634
}
633635

636+
function setupAllowedFlags() {
637+
// This builds process.allowedNodeEnvironmentFlags
638+
// from data in the config binding
639+
640+
const replaceDashesRegex = /-/g;
641+
const leadingDashesRegex = /^--?/;
642+
const trailingValuesRegex = /=.*$/;
643+
644+
// Save references so user code does not interfere
645+
const replace = Function.call.bind(String.prototype.replace);
646+
const has = Function.call.bind(Set.prototype.has);
647+
const test = Function.call.bind(RegExp.prototype.test);
648+
649+
const {
650+
allowedV8EnvironmentFlags,
651+
allowedNodeEnvironmentFlags
652+
} = process.binding('config');
653+
654+
const trimLeadingDashes = (flag) => replace(flag, leadingDashesRegex, '');
655+
656+
// Save these for comparison against flags provided to
657+
// process.allowedNodeEnvironmentFlags.has() which lack leading dashes.
658+
// Avoid interference w/ user code by flattening `Set.prototype` into
659+
// each object.
660+
const [nodeFlags, v8Flags] = [
661+
allowedNodeEnvironmentFlags, allowedV8EnvironmentFlags
662+
].map((flags) => Object.defineProperties(
663+
new Set(flags.map(trimLeadingDashes)),
664+
Object.getOwnPropertyDescriptors(Set.prototype))
665+
);
666+
667+
class NodeEnvironmentFlagsSet extends Set {
668+
constructor(...args) {
669+
super(...args);
670+
671+
// the super constructor consumes `add`, but
672+
// disallow any future adds.
673+
this.add = () => this;
674+
}
675+
676+
delete() {
677+
// noop, `Set` API compatible
678+
return false;
679+
}
680+
681+
clear() {
682+
// noop
683+
}
684+
685+
has(key) {
686+
// This will return `true` based on various possible
687+
// permutations of a flag, including present/missing leading
688+
// dash(es) and/or underscores-for-dashes in the case of V8-specific
689+
// flags. Strips any values after `=`, inclusive.
690+
if (typeof key === 'string') {
691+
key = replace(key, trailingValuesRegex, '');
692+
if (test(leadingDashesRegex, key)) {
693+
return has(this, key) ||
694+
has(v8Flags,
695+
replace(
696+
replace(
697+
key,
698+
leadingDashesRegex,
699+
''
700+
),
701+
replaceDashesRegex,
702+
'_'
703+
)
704+
);
705+
}
706+
return has(nodeFlags, key) ||
707+
has(v8Flags, replace(key, replaceDashesRegex, '_'));
708+
}
709+
return false;
710+
}
711+
}
712+
713+
Object.freeze(NodeEnvironmentFlagsSet.prototype.constructor);
714+
Object.freeze(NodeEnvironmentFlagsSet.prototype);
715+
716+
process.allowedNodeEnvironmentFlags = Object.freeze(
717+
new NodeEnvironmentFlagsSet(
718+
allowedNodeEnvironmentFlags.concat(allowedV8EnvironmentFlags)
719+
)
720+
);
721+
}
722+
634723
startup();
635724
});

src/node.cc

+62
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,68 @@ const char* signo_string(int signo) {
587587
}
588588
}
589589

590+
// These are all flags available for use with NODE_OPTIONS.
591+
//
592+
// Disallowed flags:
593+
// These flags cause Node to do things other than run scripts:
594+
// --version / -v
595+
// --eval / -e
596+
// --print / -p
597+
// --check / -c
598+
// --interactive / -i
599+
// --prof-process
600+
// --v8-options
601+
// These flags are disallowed because security:
602+
// --preserve-symlinks
603+
const char* const environment_flags[] = {
604+
// Node options, sorted in `node --help` order for ease of comparison.
605+
"--enable-fips",
606+
"--experimental-modules",
607+
"--experimenatl-repl-await",
608+
"--experimental-vm-modules",
609+
"--experimental-worker",
610+
"--force-fips",
611+
"--icu-data-dir",
612+
"--inspect",
613+
"--inspect-brk",
614+
"--inspect-port",
615+
"--loader",
616+
"--napi-modules",
617+
"--no-deprecation",
618+
"--no-force-async-hooks-checks",
619+
"--no-warnings",
620+
"--openssl-config",
621+
"--pending-deprecation",
622+
"--redirect-warnings",
623+
"--require",
624+
"--throw-deprecation",
625+
"--tls-cipher-list",
626+
"--trace-deprecation",
627+
"--trace-event-categories",
628+
"--trace-event-file-pattern",
629+
"--trace-events-enabled",
630+
"--trace-sync-io",
631+
"--trace-warnings",
632+
"--track-heap-objects",
633+
"--use-bundled-ca",
634+
"--use-openssl-ca",
635+
"--v8-pool-size",
636+
"--zero-fill-buffers",
637+
"-r"
638+
};
639+
640+
// V8 options (define with '_', which allows '-' or '_')
641+
const char* const v8_environment_flags[] = {
642+
"--abort_on_uncaught_exception",
643+
"--max_old_space_size",
644+
"--perf_basic_prof",
645+
"--perf_prof",
646+
"--stack_trace_limit",
647+
};
648+
649+
int v8_environment_flags_count = arraysize(v8_environment_flags);
650+
int environment_flags_count = arraysize(environment_flags);
651+
590652
// Look up environment variable unless running as setuid root.
591653
bool SafeGetenv(const char* key, std::string* text) {
592654
#if !defined(__CloudABI__) && !defined(_WIN32)

src/node_config.cc

+17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
namespace node {
77

8+
using v8::Array;
89
using v8::Boolean;
910
using v8::Context;
1011
using v8::Integer;
@@ -132,6 +133,22 @@ static void Initialize(Local<Object> target,
132133
READONLY_PROPERTY(debug_options_obj,
133134
"inspectorEnabled",
134135
Boolean::New(isolate, debug_options->inspector_enabled));
136+
137+
Local<Array> environmentFlags = Array::New(env->isolate(),
138+
environment_flags_count);
139+
READONLY_PROPERTY(target, "allowedNodeEnvironmentFlags", environmentFlags);
140+
for (int i = 0; i < environment_flags_count; ++i) {
141+
environmentFlags->Set(i, OneByteString(env->isolate(),
142+
environment_flags[i]));
143+
}
144+
145+
Local<Array> v8EnvironmentFlags = Array::New(env->isolate(),
146+
v8_environment_flags_count);
147+
READONLY_PROPERTY(target, "allowedV8EnvironmentFlags", v8EnvironmentFlags);
148+
for (int i = 0; i < v8_environment_flags_count; ++i) {
149+
v8EnvironmentFlags->Set(i, OneByteString(env->isolate(),
150+
v8_environment_flags[i]));
151+
}
135152
} // InitConfig
136153

137154
} // namespace node

src/node_internals.h

+5
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@ extern bool v8_initialized;
178178

179179
extern std::shared_ptr<PerProcessOptions> per_process_opts;
180180

181+
extern const char* const environment_flags[];
182+
extern int environment_flags_count;
183+
extern const char* const v8_environment_flags[];
184+
extern int v8_environment_flags_count;
185+
181186
// Forward declaration
182187
class Environment;
183188

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
require('../common');
5+
6+
// assert legit flags are allowed, and bogus flags are disallowed
7+
{
8+
const goodFlags = [
9+
'--inspect-brk',
10+
'inspect-brk',
11+
'--perf_basic_prof',
12+
'--perf-basic-prof',
13+
'perf-basic-prof',
14+
'--perf_basic-prof',
15+
'perf_basic-prof',
16+
'perf_basic_prof',
17+
'-r',
18+
'r',
19+
'--stack-trace-limit=100',
20+
'--stack-trace-limit=-=xX_nodejs_Xx=-'
21+
];
22+
23+
const badFlags = [
24+
'--inspect_brk',
25+
'INSPECT-BRK',
26+
'--INSPECT-BRK',
27+
'--r',
28+
'-R',
29+
'---inspect-brk',
30+
'--cheeseburgers'
31+
];
32+
33+
goodFlags.forEach((flag) => {
34+
assert.strictEqual(
35+
process.allowedNodeEnvironmentFlags.has(flag),
36+
true,
37+
`flag should be in set: ${flag}`
38+
);
39+
});
40+
41+
badFlags.forEach((flag) => {
42+
assert.strictEqual(
43+
process.allowedNodeEnvironmentFlags.has(flag),
44+
false,
45+
`flag should not be in set: ${flag}`
46+
);
47+
});
48+
}
49+
50+
// assert all "canonical" flags begin with dash(es)
51+
{
52+
process.allowedNodeEnvironmentFlags.forEach((flag) => {
53+
assert.strictEqual(/^--?[a-z8_-]+$/.test(flag), true);
54+
});
55+
}
56+
57+
// assert immutability of process.allowedNodeEnvironmentFlags
58+
{
59+
assert.strictEqual(Object.isFrozen(process.allowedNodeEnvironmentFlags),
60+
true);
61+
62+
process.allowedNodeEnvironmentFlags.add('foo');
63+
assert.strictEqual(process.allowedNodeEnvironmentFlags.has('foo'), false);
64+
process.allowedNodeEnvironmentFlags.forEach((flag) => {
65+
assert.strictEqual(flag === 'foo', false);
66+
});
67+
68+
process.allowedNodeEnvironmentFlags.clear();
69+
assert.strictEqual(process.allowedNodeEnvironmentFlags.size > 0, true);
70+
71+
const size = process.allowedNodeEnvironmentFlags.size;
72+
process.allowedNodeEnvironmentFlags.delete('-r');
73+
assert.strictEqual(process.allowedNodeEnvironmentFlags.size, size);
74+
}

tools/doc/type-parser.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const jsGlobalTypes = [
1818
'Array', 'ArrayBuffer', 'DataView', 'Date', 'Error', 'EvalError', 'Function',
1919
'Object', 'Promise', 'RangeError', 'ReferenceError', 'RegExp',
2020
'SharedArrayBuffer', 'SyntaxError', 'TypeError', 'TypedArray', 'URIError',
21-
'Uint8Array',
21+
'Uint8Array', 'Set'
2222
];
2323

2424
const customTypesMap = {

0 commit comments

Comments
 (0)