Skip to content

Commit 3f6199c

Browse files
committed
src: allow CLI args in env with NODE_OPTIONS
Not all CLI options are supported, those that are problematic from a security or implementation point of view are disallowed, as are ones that are inappropriate (for example, -e, -p, --i), or that only make sense when changed with code changes (such as options that change the javascript syntax or add new APIs). PR-URL: #12028 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com> Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Gibson Fahnestock <gibfahn@gmail.com> Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
1 parent d62f314 commit 3f6199c

File tree

6 files changed

+260
-38
lines changed

6 files changed

+260
-38
lines changed

configure

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,11 @@ parser.add_option('--without-ssl',
432432
dest='without_ssl',
433433
help='build without SSL')
434434

435+
parser.add_option('--without-node-options',
436+
action='store_true',
437+
dest='without_node_options',
438+
help='build without NODE_OPTIONS support')
439+
435440
parser.add_option('--xcode',
436441
action='store_true',
437442
dest='use_xcode',
@@ -965,6 +970,9 @@ def configure_openssl(o):
965970
o['variables']['openssl_no_asm'] = 1 if options.openssl_no_asm else 0
966971
if options.use_openssl_ca_store:
967972
o['defines'] += ['NODE_OPENSSL_CERT_STORE']
973+
o['variables']['node_without_node_options'] = b(options.without_node_options)
974+
if options.without_node_options:
975+
o['defines'] += ['NODE_WITHOUT_NODE_OPTIONS']
968976
if options.openssl_fips:
969977
o['variables']['openssl_fips'] = options.openssl_fips
970978
fips_dir = os.path.join(root_dir, 'deps', 'openssl', 'fips')

doc/api/cli.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,40 @@ added: v7.5.0
365365

366366
When set to `1`, process warnings are silenced.
367367

368+
### `NODE_OPTIONS=options...`
369+
<!-- YAML
370+
added: REPLACEME
371+
-->
372+
373+
`options...` are interpreted as if they had been specified on the command line
374+
before the actual command line (so they can be overriden). Node will exit with
375+
an error if an option that is not allowed in the environment is used, such as
376+
`-p` or a script file.
377+
378+
Node options that are allowed are:
379+
- `--enable-fips`
380+
- `--force-fips`
381+
- `--icu-data-dir`
382+
- `--no-deprecation`
383+
- `--no-warnings`
384+
- `--openssl-config`
385+
- `--prof-process`
386+
- `--redirect-warnings`
387+
- `--require`, `-r`
388+
- `--throw-deprecation`
389+
- `--trace-deprecation`
390+
- `--trace-events-enabled`
391+
- `--trace-sync-io`
392+
- `--trace-warnings`
393+
- `--track-heap-objects`
394+
- `--use-bundled-ca`
395+
- `--use-openssl-ca`
396+
- `--v8-pool-size`
397+
- `--zero-fill-buffers`
398+
399+
V8 options that are allowed are:
400+
- `--max_old_space_size`
401+
368402
### `NODE_PRESERVE_SYMLINKS=1`
369403
<!-- YAML
370404
added: v7.1.0

doc/node.1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ with small\-icu support.
237237
.BR NODE_NO_WARNINGS =\fI1\fR
238238
When set to \fI1\fR, process warnings are silenced.
239239

240+
.TP
241+
.BR NODE_OPTIONS =\fIoptions...\fR
242+
\fBoptions...\fR are interpreted as if they had been specified on the command
243+
line before the actual command line (so they can be overriden). Node will exit
244+
with an error if an option that is not allowed in the environment is used, such
245+
as \fB-p\fR or a script file.
246+
240247
.TP
241248
.BR NODE_PATH =\fIpath\fR[:\fI...\fR]
242249
\':\'\-separated list of directories prefixed to the module search path.

src/node.cc

Lines changed: 133 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3548,6 +3548,9 @@ static void PrintHelp() {
35483548
#endif
35493549
#endif
35503550
"NODE_NO_WARNINGS set to 1 to silence process warnings\n"
3551+
#if !defined(NODE_WITHOUT_NODE_OPTIONS)
3552+
"NODE_OPTIONS set CLI options in the environment\n"
3553+
#endif
35513554
#ifdef _WIN32
35523555
"NODE_PATH ';'-separated list of directories\n"
35533556
#else
@@ -3562,6 +3565,51 @@ static void PrintHelp() {
35623565
}
35633566

35643567

3568+
static void CheckIfAllowedInEnv(const char* exe, bool is_env,
3569+
const char* arg) {
3570+
if (!is_env)
3571+
return;
3572+
3573+
// Find the arg prefix when its --some_arg=val
3574+
const char* eq = strchr(arg, '=');
3575+
size_t arglen = eq ? eq - arg : strlen(arg);
3576+
3577+
static const char* whitelist[] = {
3578+
// Node options
3579+
"-r", "--require",
3580+
"--no-deprecation",
3581+
"--no-warnings",
3582+
"--trace-warnings",
3583+
"--redirect-warnings",
3584+
"--trace-deprecation",
3585+
"--trace-sync-io",
3586+
"--trace-events-enabled",
3587+
"--track-heap-objects",
3588+
"--throw-deprecation",
3589+
"--zero-fill-buffers",
3590+
"--v8-pool-size",
3591+
"--use-openssl-ca",
3592+
"--use-bundled-ca",
3593+
"--enable-fips",
3594+
"--force-fips",
3595+
"--openssl-config",
3596+
"--icu-data-dir",
3597+
3598+
// V8 options
3599+
"--max_old_space_size",
3600+
};
3601+
3602+
for (unsigned i = 0; i < arraysize(whitelist); i++) {
3603+
const char* allowed = whitelist[i];
3604+
if (strlen(allowed) == arglen && strncmp(allowed, arg, arglen) == 0)
3605+
return;
3606+
}
3607+
3608+
fprintf(stderr, "%s: %s is not allowed in NODE_OPTIONS\n", exe, arg);
3609+
exit(9);
3610+
}
3611+
3612+
35653613
// Parse command line arguments.
35663614
//
35673615
// argv is modified in place. exec_argv and v8_argv are out arguments that
@@ -3578,7 +3626,8 @@ static void ParseArgs(int* argc,
35783626
int* exec_argc,
35793627
const char*** exec_argv,
35803628
int* v8_argc,
3581-
const char*** v8_argv) {
3629+
const char*** v8_argv,
3630+
bool is_env) {
35823631
const unsigned int nargs = static_cast<unsigned int>(*argc);
35833632
const char** new_exec_argv = new const char*[nargs];
35843633
const char** new_v8_argv = new const char*[nargs];
@@ -3605,6 +3654,8 @@ static void ParseArgs(int* argc,
36053654
const char* const arg = argv[index];
36063655
unsigned int args_consumed = 1;
36073656

3657+
CheckIfAllowedInEnv(argv[0], is_env, arg);
3658+
36083659
if (debug_options.ParseOption(arg)) {
36093660
// Done, consumed by DebugOptions::ParseOption().
36103661
} else if (strcmp(arg, "--version") == 0 || strcmp(arg, "-v") == 0) {
@@ -3733,6 +3784,13 @@ static void ParseArgs(int* argc,
37333784

37343785
// Copy remaining arguments.
37353786
const unsigned int args_left = nargs - index;
3787+
3788+
if (is_env && args_left) {
3789+
fprintf(stderr, "%s: %s is not supported in NODE_OPTIONS\n",
3790+
argv[0], argv[index]);
3791+
exit(9);
3792+
}
3793+
37363794
memcpy(new_argv + new_argc, argv + index, args_left * sizeof(*argv));
37373795
new_argc += args_left;
37383796

@@ -4166,6 +4224,54 @@ inline void PlatformInit() {
41664224
}
41674225

41684226

4227+
void ProcessArgv(int* argc,
4228+
const char** argv,
4229+
int* exec_argc,
4230+
const char*** exec_argv,
4231+
bool is_env = false) {
4232+
// Parse a few arguments which are specific to Node.
4233+
int v8_argc;
4234+
const char** v8_argv;
4235+
ParseArgs(argc, argv, exec_argc, exec_argv, &v8_argc, &v8_argv, is_env);
4236+
4237+
// TODO(bnoordhuis) Intercept --prof arguments and start the CPU profiler
4238+
// manually? That would give us a little more control over its runtime
4239+
// behavior but it could also interfere with the user's intentions in ways
4240+
// we fail to anticipate. Dillema.
4241+
for (int i = 1; i < v8_argc; ++i) {
4242+
if (strncmp(v8_argv[i], "--prof", sizeof("--prof") - 1) == 0) {
4243+
v8_is_profiling = true;
4244+
break;
4245+
}
4246+
}
4247+
4248+
#ifdef __POSIX__
4249+
// Block SIGPROF signals when sleeping in epoll_wait/kevent/etc. Avoids the
4250+
// performance penalty of frequent EINTR wakeups when the profiler is running.
4251+
// Only do this for v8.log profiling, as it breaks v8::CpuProfiler users.
4252+
if (v8_is_profiling) {
4253+
uv_loop_configure(uv_default_loop(), UV_LOOP_BLOCK_SIGNAL, SIGPROF);
4254+
}
4255+
#endif
4256+
4257+
// The const_cast doesn't violate conceptual const-ness. V8 doesn't modify
4258+
// the argv array or the elements it points to.
4259+
if (v8_argc > 1)
4260+
V8::SetFlagsFromCommandLine(&v8_argc, const_cast<char**>(v8_argv), true);
4261+
4262+
// Anything that's still in v8_argv is not a V8 or a node option.
4263+
for (int i = 1; i < v8_argc; i++) {
4264+
fprintf(stderr, "%s: bad option: %s\n", argv[0], v8_argv[i]);
4265+
}
4266+
delete[] v8_argv;
4267+
v8_argv = nullptr;
4268+
4269+
if (v8_argc > 1) {
4270+
exit(9);
4271+
}
4272+
}
4273+
4274+
41694275
void Init(int* argc,
41704276
const char** argv,
41714277
int* exec_argc,
@@ -4208,31 +4314,36 @@ void Init(int* argc,
42084314
if (openssl_config.empty())
42094315
SafeGetenv("OPENSSL_CONF", &openssl_config);
42104316

4211-
// Parse a few arguments which are specific to Node.
4212-
int v8_argc;
4213-
const char** v8_argv;
4214-
ParseArgs(argc, argv, exec_argc, exec_argv, &v8_argc, &v8_argv);
4215-
4216-
// TODO(bnoordhuis) Intercept --prof arguments and start the CPU profiler
4217-
// manually? That would give us a little more control over its runtime
4218-
// behavior but it could also interfere with the user's intentions in ways
4219-
// we fail to anticipate. Dillema.
4220-
for (int i = 1; i < v8_argc; ++i) {
4221-
if (strncmp(v8_argv[i], "--prof", sizeof("--prof") - 1) == 0) {
4222-
v8_is_profiling = true;
4223-
break;
4317+
#if !defined(NODE_WITHOUT_NODE_OPTIONS)
4318+
std::string node_options;
4319+
if (SafeGetenv("NODE_OPTIONS", &node_options)) {
4320+
// Smallest tokens are 2-chars (a not space and a space), plus 2 extra
4321+
// pointers, for the prepended executable name, and appended NULL pointer.
4322+
size_t max_len = 2 + (node_options.length() + 1) / 2;
4323+
const char** argv_from_env = new const char*[max_len];
4324+
int argc_from_env = 0;
4325+
// [0] is expected to be the program name, fill it in from the real argv.
4326+
argv_from_env[argc_from_env++] = argv[0];
4327+
4328+
char* cstr = strdup(node_options.c_str());
4329+
char* initptr = cstr;
4330+
char* token;
4331+
while ((token = strtok(initptr, " "))) { // NOLINT(runtime/threadsafe_fn)
4332+
initptr = nullptr;
4333+
argv_from_env[argc_from_env++] = token;
42244334
}
4225-
}
4226-
4227-
#ifdef __POSIX__
4228-
// Block SIGPROF signals when sleeping in epoll_wait/kevent/etc. Avoids the
4229-
// performance penalty of frequent EINTR wakeups when the profiler is running.
4230-
// Only do this for v8.log profiling, as it breaks v8::CpuProfiler users.
4231-
if (v8_is_profiling) {
4232-
uv_loop_configure(uv_default_loop(), UV_LOOP_BLOCK_SIGNAL, SIGPROF);
4335+
argv_from_env[argc_from_env] = nullptr;
4336+
int exec_argc_;
4337+
const char** exec_argv_ = nullptr;
4338+
ProcessArgv(&argc_from_env, argv_from_env, &exec_argc_, &exec_argv_, true);
4339+
delete[] exec_argv_;
4340+
delete[] argv_from_env;
4341+
free(cstr);
42334342
}
42344343
#endif
42354344

4345+
ProcessArgv(argc, argv, exec_argc, exec_argv);
4346+
42364347
#if defined(NODE_HAVE_I18N_SUPPORT)
42374348
// If the parameter isn't given, use the env variable.
42384349
if (icu_data_dir.empty())
@@ -4244,21 +4355,6 @@ void Init(int* argc,
42444355
"(check NODE_ICU_DATA or --icu-data-dir parameters)");
42454356
}
42464357
#endif
4247-
// The const_cast doesn't violate conceptual const-ness. V8 doesn't modify
4248-
// the argv array or the elements it points to.
4249-
if (v8_argc > 1)
4250-
V8::SetFlagsFromCommandLine(&v8_argc, const_cast<char**>(v8_argv), true);
4251-
4252-
// Anything that's still in v8_argv is not a V8 or a node option.
4253-
for (int i = 1; i < v8_argc; i++) {
4254-
fprintf(stderr, "%s: bad option: %s\n", argv[0], v8_argv[i]);
4255-
}
4256-
delete[] v8_argv;
4257-
v8_argv = nullptr;
4258-
4259-
if (v8_argc > 1) {
4260-
exit(9);
4261-
}
42624358

42634359
// Unconditionally force typed arrays to allocate outside the v8 heap. This
42644360
// is to prevent memory pointers from being moved around that are returned by
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (process.config.variables.node_without_node_options)
4+
return common.skip('missing NODE_OPTIONS support');
5+
6+
// Test options specified by env variable.
7+
8+
const assert = require('assert');
9+
const exec = require('child_process').execFile;
10+
11+
disallow('--version');
12+
disallow('-v');
13+
disallow('--help');
14+
disallow('-h');
15+
disallow('--eval');
16+
disallow('-e');
17+
disallow('--print');
18+
disallow('-p');
19+
disallow('-pe');
20+
disallow('--check');
21+
disallow('-c');
22+
disallow('--interactive');
23+
disallow('-i');
24+
disallow('--v8-options');
25+
disallow('--');
26+
27+
function disallow(opt) {
28+
const options = {env: {NODE_OPTIONS: opt}};
29+
exec(process.execPath, options, common.mustCall(function(err) {
30+
const message = err.message.split(/\r?\n/)[1];
31+
const expect = process.execPath + ': ' + opt +
32+
' is not allowed in NODE_OPTIONS';
33+
34+
assert.strictEqual(err.code, 9);
35+
assert.strictEqual(message, expect);
36+
}));
37+
}
38+
39+
const printA = require.resolve('../fixtures/printA.js');
40+
41+
expect('-r ' + printA, 'A\nB\n');
42+
expect('--no-deprecation', 'B\n');
43+
expect('--no-warnings', 'B\n');
44+
expect('--trace-warnings', 'B\n');
45+
expect('--trace-deprecation', 'B\n');
46+
expect('--trace-sync-io', 'B\n');
47+
expect('--trace-events-enabled', 'B\n');
48+
expect('--track-heap-objects', 'B\n');
49+
expect('--throw-deprecation', 'B\n');
50+
expect('--zero-fill-buffers', 'B\n');
51+
expect('--v8-pool-size=10', 'B\n');
52+
expect('--use-openssl-ca', 'B\n');
53+
expect('--use-bundled-ca', 'B\n');
54+
expect('--openssl-config=_ossl_cfg', 'B\n');
55+
expect('--icu-data-dir=_d', 'B\n');
56+
57+
// V8 options
58+
expect('--max_old_space_size=0', 'B\n');
59+
60+
function expect(opt, want) {
61+
const printB = require.resolve('../fixtures/printB.js');
62+
const argv = [printB];
63+
const opts = {
64+
env: {NODE_OPTIONS: opt},
65+
maxBuffer: 1000000000,
66+
};
67+
exec(process.execPath, argv, opts, common.mustCall(function(err, stdout) {
68+
assert.ifError(err);
69+
if (!RegExp(want).test(stdout)) {
70+
console.error('For %j, failed to find %j in: <\n%s\n>',
71+
opt, expect, stdout);
72+
assert(false, 'Expected ' + expect);
73+
}
74+
}));
75+
}

0 commit comments

Comments
 (0)