Skip to content

Commit dafa9c7

Browse files
devsnektargos
authored andcommitted
lib: add option to disable __proto__
Adds `--disable-proto` CLI option which can be set to `delete` or `throw`. Fixes nodejs#31951 PR-URL: nodejs#32279 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: David Carlier <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: Vladimir de Turckheim <[email protected]>
1 parent 9225c64 commit dafa9c7

File tree

10 files changed

+174
-29
lines changed

10 files changed

+174
-29
lines changed

doc/api/cli.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ added: v12.0.0
127127
128128
Specify the file name of the CPU profile generated by `--cpu-prof`.
129129

130+
### `--disable-proto=mode`
131+
<!--YAML
132+
added: REPLACEME
133+
-->
134+
135+
Disable the `Object.prototype.__proto__` property. If `mode` is `delete`, the
136+
property will be removed entirely. If `mode` is `throw`, accesses to the
137+
property will throw an exception with the code `ERR_PROTO_ACCESS`.
138+
130139
### `--disallow-code-generation-from-strings`
131140
<!-- YAML
132141
added: v9.8.0
@@ -1131,6 +1140,7 @@ node --require "./a.js" --require "./b.js"
11311140

11321141
Node.js options that are allowed are:
11331142
<!-- node-options-node start -->
1143+
* `--disable-proto`
11341144
* `--enable-fips`
11351145
* `--enable-source-maps`
11361146
* `--experimental-import-meta-resolve`

doc/api/errors.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1664,6 +1664,14 @@ The `package.json` [exports][] field does not export the requested subpath.
16641664
Because exports are encapsulated, private internal modules that are not exported
16651665
cannot be imported through the package resolution, unless using an absolute URL.
16661666

1667+
<a id="ERR_PROTO_ACCESS"></a>
1668+
### `ERR_PROTO_ACCESS`
1669+
1670+
Accessing `Object.prototype.__proto__` has been forbidden using
1671+
[`--disable-proto=throw`][]. [`Object.getPrototypeOf`][] and
1672+
[`Object.setPrototypeOf`][] should be used to get and set the prototype of an
1673+
object.
1674+
16671675
<a id="ERR_REQUIRE_ESM"></a>
16681676
### `ERR_REQUIRE_ESM`
16691677

@@ -2474,10 +2482,13 @@ This `Error` is thrown when a read is attempted on a TTY `WriteStream`,
24742482
such as `process.stdout.on('data')`.
24752483

24762484
[`'uncaughtException'`]: process.html#process_event_uncaughtexception
2485+
[`--disable-proto=throw`]: cli.html#cli_disable_proto_mode
24772486
[`--force-fips`]: cli.html#cli_force_fips
24782487
[`Class: assert.AssertionError`]: assert.html#assert_class_assert_assertionerror
24792488
[`ERR_INVALID_ARG_TYPE`]: #ERR_INVALID_ARG_TYPE
24802489
[`EventEmitter`]: events.html#events_class_eventemitter
2490+
[`Object.getPrototypeOf`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf
2491+
[`Object.setPrototypeOf`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf
24812492
[`REPL`]: repl.html
24822493
[`Writable`]: stream.html#stream_class_stream_writable
24832494
[`child_process`]: child_process.html

doc/node.1

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ The default is
100100
File name of the V8 CPU profile generated with
101101
.Fl -cpu-prof
102102
.
103+
.It Fl -disable-proto Ns = Ns Ar mode
104+
Disable the `Object.prototype.__proto__` property. If
105+
.Ar mode
106+
is `delete`, the property will be removed entirely. If
107+
.Ar mode
108+
is `throw`, accesses to the property will throw an exception with the code
109+
`ERR_PROTO_ACCESS`.
110+
.
103111
.It Fl -disallow-code-generation-from-strings
104112
Make built-in language features like `eval` and `new Function` that generate
105113
code from strings throw an exception instead. This does not affect the Node.js

src/api/environment.cc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ using v8::Context;
1414
using v8::EscapableHandleScope;
1515
using v8::FinalizationGroup;
1616
using v8::Function;
17+
using v8::FunctionCallbackInfo;
1718
using v8::HandleScope;
1819
using v8::Isolate;
1920
using v8::Local;
@@ -23,6 +24,7 @@ using v8::Null;
2324
using v8::Object;
2425
using v8::ObjectTemplate;
2526
using v8::Private;
27+
using v8::PropertyDescriptor;
2628
using v8::String;
2729
using v8::Value;
2830

@@ -403,6 +405,10 @@ Local<Context> NewContext(Isolate* isolate,
403405
return context;
404406
}
405407

408+
void ProtoThrower(const FunctionCallbackInfo<Value>& info) {
409+
THROW_ERR_PROTO_ACCESS(info.GetIsolate());
410+
}
411+
406412
// This runs at runtime, regardless of whether the context
407413
// is created from a snapshot.
408414
void InitializeContextRuntime(Local<Context> context) {
@@ -431,6 +437,32 @@ void InitializeContextRuntime(Local<Context> context) {
431437
Local<Object> atomics = atomics_v.As<Object>();
432438
atomics->Delete(context, wake_string).FromJust();
433439
}
440+
441+
// Remove __proto__
442+
// https://github.com/nodejs/node/issues/31951
443+
Local<String> object_string = FIXED_ONE_BYTE_STRING(isolate, "Object");
444+
Local<String> prototype_string = FIXED_ONE_BYTE_STRING(isolate, "prototype");
445+
Local<Object> prototype = context->Global()
446+
->Get(context, object_string)
447+
.ToLocalChecked()
448+
.As<Object>()
449+
->Get(context, prototype_string)
450+
.ToLocalChecked()
451+
.As<Object>();
452+
Local<String> proto_string = FIXED_ONE_BYTE_STRING(isolate, "__proto__");
453+
if (per_process::cli_options->disable_proto == "delete") {
454+
prototype->Delete(context, proto_string).ToChecked();
455+
} else if (per_process::cli_options->disable_proto == "throw") {
456+
Local<Value> thrower =
457+
Function::New(context, ProtoThrower).ToLocalChecked();
458+
PropertyDescriptor descriptor(thrower, thrower);
459+
descriptor.set_enumerable(false);
460+
descriptor.set_configurable(true);
461+
prototype->DefineProperty(context, proto_string, descriptor).ToChecked();
462+
} else if (per_process::cli_options->disable_proto != "") {
463+
// Validated in ProcessGlobalArgs
464+
FatalError("InitializeContextRuntime()", "invalid --disable-proto mode");
465+
}
434466
}
435467

436468
bool InitializeContextForSnapshot(Local<Context> context) {

src/node.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,13 @@ int ProcessGlobalArgs(std::vector<std::string>* args,
670670
}
671671
}
672672

673+
if (per_process::cli_options->disable_proto != "delete" &&
674+
per_process::cli_options->disable_proto != "throw" &&
675+
per_process::cli_options->disable_proto != "") {
676+
errors->emplace_back("invalid mode passed to --disable-proto");
677+
return 12;
678+
}
679+
673680
auto env_opts = per_process::cli_options->per_isolate->per_env;
674681
if (std::find(v8_args.begin(), v8_args.end(),
675682
"--abort-on-uncaught-exception") != v8_args.end() ||

src/node_errors.h

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,34 @@ void OnFatalError(const char* location, const char* message);
3131
// `node::ERR_INVALID_ARG_TYPE(isolate, "message")` returning
3232
// a `Local<Value>` containing the TypeError with proper code and message
3333

34-
#define ERRORS_WITH_CODE(V) \
35-
V(ERR_BUFFER_CONTEXT_NOT_AVAILABLE, Error) \
36-
V(ERR_BUFFER_OUT_OF_BOUNDS, RangeError) \
37-
V(ERR_BUFFER_TOO_LARGE, Error) \
38-
V(ERR_CONSTRUCT_CALL_REQUIRED, TypeError) \
39-
V(ERR_CONSTRUCT_CALL_INVALID, TypeError) \
40-
V(ERR_CRYPTO_UNKNOWN_CIPHER, Error) \
41-
V(ERR_CRYPTO_UNKNOWN_DH_GROUP, Error) \
42-
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \
43-
V(ERR_INVALID_ARG_VALUE, TypeError) \
44-
V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \
45-
V(ERR_INVALID_ARG_TYPE, TypeError) \
46-
V(ERR_INVALID_TRANSFER_OBJECT, TypeError) \
47-
V(ERR_MEMORY_ALLOCATION_FAILED, Error) \
48-
V(ERR_MISSING_ARGS, TypeError) \
49-
V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, TypeError) \
50-
V(ERR_MISSING_PASSPHRASE, TypeError) \
51-
V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \
52-
V(ERR_NON_CONTEXT_AWARE_DISABLED, Error) \
53-
V(ERR_OUT_OF_RANGE, RangeError) \
54-
V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \
55-
V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \
56-
V(ERR_STRING_TOO_LONG, Error) \
57-
V(ERR_TLS_INVALID_PROTOCOL_METHOD, TypeError) \
58-
V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, TypeError) \
59-
V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, Error) \
60-
V(ERR_VM_MODULE_CACHED_DATA_REJECTED, Error) \
34+
#define ERRORS_WITH_CODE(V) \
35+
V(ERR_BUFFER_CONTEXT_NOT_AVAILABLE, Error) \
36+
V(ERR_BUFFER_OUT_OF_BOUNDS, RangeError) \
37+
V(ERR_BUFFER_TOO_LARGE, Error) \
38+
V(ERR_CONSTRUCT_CALL_REQUIRED, TypeError) \
39+
V(ERR_CONSTRUCT_CALL_INVALID, TypeError) \
40+
V(ERR_CRYPTO_UNKNOWN_CIPHER, Error) \
41+
V(ERR_CRYPTO_UNKNOWN_DH_GROUP, Error) \
42+
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \
43+
V(ERR_INVALID_ARG_VALUE, TypeError) \
44+
V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \
45+
V(ERR_INVALID_ARG_TYPE, TypeError) \
46+
V(ERR_INVALID_TRANSFER_OBJECT, TypeError) \
47+
V(ERR_MEMORY_ALLOCATION_FAILED, Error) \
48+
V(ERR_MISSING_ARGS, TypeError) \
49+
V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, TypeError) \
50+
V(ERR_MISSING_PASSPHRASE, TypeError) \
51+
V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \
52+
V(ERR_NON_CONTEXT_AWARE_DISABLED, Error) \
53+
V(ERR_OUT_OF_RANGE, RangeError) \
54+
V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \
55+
V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \
56+
V(ERR_STRING_TOO_LONG, Error) \
57+
V(ERR_TLS_INVALID_PROTOCOL_METHOD, TypeError) \
58+
V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, TypeError) \
59+
V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, Error) \
60+
V(ERR_VM_MODULE_CACHED_DATA_REJECTED, Error) \
61+
V(ERR_PROTO_ACCESS, Error)
6162

6263
#define V(code, type) \
6364
inline v8::Local<v8::Value> code(v8::Isolate* isolate, \
@@ -105,7 +106,10 @@ void OnFatalError(const char* location, const char* message);
105106
"Script execution was interrupted by `SIGINT`") \
106107
V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, \
107108
"Cannot serialize externalized SharedArrayBuffer") \
108-
V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, "Failed to set PSK identity hint")
109+
V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, "Failed to set PSK identity hint") \
110+
V(ERR_PROTO_ACCESS, \
111+
"Accessing Object.prototype.__proto__ has been " \
112+
"disallowed with --disable-proto=throw")
109113

110114
#define V(code, message) \
111115
inline v8::Local<v8::Value> code(v8::Isolate* isolate) { \

src/node_options.cc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,10 @@ PerProcessOptionsParser::PerProcessOptionsParser(
652652
"", /* undocumented, only for debugging */
653653
&PerProcessOptions::debug_arraybuffer_allocations,
654654
kAllowedInEnvironment);
655-
655+
AddOption("--disable-proto",
656+
"disable Object.prototype.__proto__",
657+
&PerProcessOptions::disable_proto,
658+
kAllowedInEnvironment);
656659

657660
// 12.x renamed this inadvertently, so alias it for consistency within the
658661
// release line, while using the original name for consistency with older

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ class PerProcessOptions : public Options {
207207
int64_t v8_thread_pool_size = 4;
208208
bool zero_fill_all_buffers = false;
209209
bool debug_arraybuffer_allocations = false;
210+
std::string disable_proto;
210211

211212
std::vector<std::string> security_reverts;
212213
bool print_bash_completion = false;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Flags: --disable-proto=delete
2+
3+
'use strict';
4+
5+
require('../common');
6+
const assert = require('assert');
7+
const vm = require('vm');
8+
const { Worker, isMainThread } = require('worker_threads');
9+
10+
// eslint-disable-next-line no-proto
11+
assert.strictEqual(Object.prototype.__proto__, undefined);
12+
assert(!Object.prototype.hasOwnProperty('__proto__'));
13+
14+
const ctx = vm.createContext();
15+
const ctxGlobal = vm.runInContext('this', ctx);
16+
17+
// eslint-disable-next-line no-proto
18+
assert.strictEqual(ctxGlobal.Object.prototype.__proto__, undefined);
19+
assert(!ctxGlobal.Object.prototype.hasOwnProperty('__proto__'));
20+
21+
if (isMainThread) {
22+
new Worker(__filename);
23+
} else {
24+
process.exit();
25+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Flags: --disable-proto=throw
2+
3+
'use strict';
4+
5+
require('../common');
6+
const assert = require('assert');
7+
const vm = require('vm');
8+
const { Worker, isMainThread } = require('worker_threads');
9+
10+
assert(Object.prototype.hasOwnProperty('__proto__'));
11+
12+
assert.throws(() => {
13+
// eslint-disable-next-line no-proto
14+
({}).__proto__;
15+
}, {
16+
code: 'ERR_PROTO_ACCESS'
17+
});
18+
19+
assert.throws(() => {
20+
// eslint-disable-next-line no-proto
21+
({}).__proto__ = {};
22+
}, {
23+
code: 'ERR_PROTO_ACCESS',
24+
});
25+
26+
const ctx = vm.createContext();
27+
28+
assert.throws(() => {
29+
vm.runInContext('({}).__proto__;', ctx);
30+
}, {
31+
code: 'ERR_PROTO_ACCESS'
32+
});
33+
34+
assert.throws(() => {
35+
vm.runInContext('({}).__proto__ = {};', ctx);
36+
}, {
37+
code: 'ERR_PROTO_ACCESS',
38+
});
39+
40+
if (isMainThread) {
41+
new Worker(__filename);
42+
} else {
43+
process.exit();
44+
}

0 commit comments

Comments
 (0)