Skip to content

Commit 36ba54e

Browse files
devsnekMylesBorins
authored andcommitted
lib: add option to disable __proto__
Adds `--disable-proto` CLI option which can be set to `delete` or `throw`. Fixes #31951 PR-URL: #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 ef32069 commit 36ba54e

10 files changed

+174
-29
lines changed

doc/api/cli.md

+10
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
@@ -1109,6 +1118,7 @@ node --require "./a.js" --require "./b.js"
11091118

11101119
Node.js options that are allowed are:
11111120
<!-- node-options-node start -->
1121+
* `--disable-proto`
11121122
* `--enable-fips`
11131123
* `--enable-source-maps`
11141124
* `--experimental-import-meta-resolve`

doc/api/errors.md

+11
Original file line numberDiff line numberDiff line change
@@ -1674,6 +1674,14 @@ The `package.json` [exports][] field does not export the requested subpath.
16741674
Because exports are encapsulated, private internal modules that are not exported
16751675
cannot be imported through the package resolution, unless using an absolute URL.
16761676

1677+
<a id="ERR_PROTO_ACCESS"></a>
1678+
### `ERR_PROTO_ACCESS`
1679+
1680+
Accessing `Object.prototype.__proto__` has been forbidden using
1681+
[`--disable-proto=throw`][]. [`Object.getPrototypeOf`][] and
1682+
[`Object.setPrototypeOf`][] should be used to get and set the prototype of an
1683+
object.
1684+
16771685
<a id="ERR_REQUIRE_ESM"></a>
16781686
### `ERR_REQUIRE_ESM`
16791687

@@ -2490,10 +2498,13 @@ This `Error` is thrown when a read is attempted on a TTY `WriteStream`,
24902498
such as `process.stdout.on('data')`.
24912499

24922500
[`'uncaughtException'`]: process.html#process_event_uncaughtexception
2501+
[`--disable-proto=throw`]: cli.html#cli_disable_proto_mode
24932502
[`--force-fips`]: cli.html#cli_force_fips
24942503
[`Class: assert.AssertionError`]: assert.html#assert_class_assert_assertionerror
24952504
[`ERR_INVALID_ARG_TYPE`]: #ERR_INVALID_ARG_TYPE
24962505
[`EventEmitter`]: events.html#events_class_eventemitter
2506+
[`Object.getPrototypeOf`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf
2507+
[`Object.setPrototypeOf`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf
24972508
[`REPL`]: repl.html
24982509
[`Writable`]: stream.html#stream_class_stream_writable
24992510
[`child_process`]: child_process.html

doc/node.1

+8
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

+32
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

@@ -417,6 +419,10 @@ Local<Context> NewContext(Isolate* isolate,
417419
return context;
418420
}
419421

422+
void ProtoThrower(const FunctionCallbackInfo<Value>& info) {
423+
THROW_ERR_PROTO_ACCESS(info.GetIsolate());
424+
}
425+
420426
// This runs at runtime, regardless of whether the context
421427
// is created from a snapshot.
422428
void InitializeContextRuntime(Local<Context> context) {
@@ -445,6 +451,32 @@ void InitializeContextRuntime(Local<Context> context) {
445451
Local<Object> atomics = atomics_v.As<Object>();
446452
atomics->Delete(context, wake_string).FromJust();
447453
}
454+
455+
// Remove __proto__
456+
// https://github.com/nodejs/node/issues/31951
457+
Local<String> object_string = FIXED_ONE_BYTE_STRING(isolate, "Object");
458+
Local<String> prototype_string = FIXED_ONE_BYTE_STRING(isolate, "prototype");
459+
Local<Object> prototype = context->Global()
460+
->Get(context, object_string)
461+
.ToLocalChecked()
462+
.As<Object>()
463+
->Get(context, prototype_string)
464+
.ToLocalChecked()
465+
.As<Object>();
466+
Local<String> proto_string = FIXED_ONE_BYTE_STRING(isolate, "__proto__");
467+
if (per_process::cli_options->disable_proto == "delete") {
468+
prototype->Delete(context, proto_string).ToChecked();
469+
} else if (per_process::cli_options->disable_proto == "throw") {
470+
Local<Value> thrower =
471+
Function::New(context, ProtoThrower).ToLocalChecked();
472+
PropertyDescriptor descriptor(thrower, thrower);
473+
descriptor.set_enumerable(false);
474+
descriptor.set_configurable(true);
475+
prototype->DefineProperty(context, proto_string, descriptor).ToChecked();
476+
} else if (per_process::cli_options->disable_proto != "") {
477+
// Validated in ProcessGlobalArgs
478+
FatalError("InitializeContextRuntime()", "invalid --disable-proto mode");
479+
}
448480
}
449481

450482
bool InitializeContextForSnapshot(Local<Context> context) {

src/node.cc

+7
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,13 @@ int ProcessGlobalArgs(std::vector<std::string>* args,
718718
}
719719
}
720720

721+
if (per_process::cli_options->disable_proto != "delete" &&
722+
per_process::cli_options->disable_proto != "throw" &&
723+
per_process::cli_options->disable_proto != "") {
724+
errors->emplace_back("invalid mode passed to --disable-proto");
725+
return 12;
726+
}
727+
721728
auto env_opts = per_process::cli_options->per_isolate->per_env;
722729
if (std::find(v8_args.begin(), v8_args.end(),
723730
"--abort-on-uncaught-exception") != v8_args.end() ||

src/node_errors.h

+32-28
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

+4-1
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,10 @@ PerProcessOptionsParser::PerProcessOptionsParser(
639639
"", /* undocumented, only for debugging */
640640
&PerProcessOptions::debug_arraybuffer_allocations,
641641
kAllowedInEnvironment);
642-
642+
AddOption("--disable-proto",
643+
"disable Object.prototype.__proto__",
644+
&PerProcessOptions::disable_proto,
645+
kAllowedInEnvironment);
643646

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

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ class PerProcessOptions : public Options {
205205
int64_t v8_thread_pool_size = 4;
206206
bool zero_fill_all_buffers = false;
207207
bool debug_arraybuffer_allocations = false;
208+
std::string disable_proto;
208209

209210
std::vector<std::string> security_reverts;
210211
bool print_bash_completion = false;
+25
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+
}
+44
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)