diff --git a/doc/api/cli.md b/doc/api/cli.md index aedc223f9851fb..28615b12fbba0b 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -127,6 +127,15 @@ added: v12.0.0 Specify the file name of the CPU profile generated by `--cpu-prof`. +### `--disable-proto=mode` + + +Disable the `Object.prototype.__proto__` property. If `mode` is `delete`, the +property will be removed entirely. If `mode` is `throw`, accesses to the +property will throw an exception with the code `ERR_PROTO_ACCESS`. + ### `--disallow-code-generation-from-strings` +* `--disable-proto` * `--enable-fips` * `--enable-source-maps` * `--experimental-import-meta-resolve` diff --git a/doc/api/errors.md b/doc/api/errors.md index 4cc4a29b736ad5..4c6b4221aa8ff3 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1664,6 +1664,14 @@ The `package.json` [exports][] field does not export the requested subpath. Because exports are encapsulated, private internal modules that are not exported cannot be imported through the package resolution, unless using an absolute URL. + +### `ERR_PROTO_ACCESS` + +Accessing `Object.prototype.__proto__` has been forbidden using +[`--disable-proto=throw`][]. [`Object.getPrototypeOf`][] and +[`Object.setPrototypeOf`][] should be used to get and set the prototype of an +object. + ### `ERR_REQUIRE_ESM` @@ -2474,10 +2482,13 @@ This `Error` is thrown when a read is attempted on a TTY `WriteStream`, such as `process.stdout.on('data')`. [`'uncaughtException'`]: process.html#process_event_uncaughtexception +[`--disable-proto=throw`]: cli.html#cli_disable_proto_mode [`--force-fips`]: cli.html#cli_force_fips [`Class: assert.AssertionError`]: assert.html#assert_class_assert_assertionerror [`ERR_INVALID_ARG_TYPE`]: #ERR_INVALID_ARG_TYPE [`EventEmitter`]: events.html#events_class_eventemitter +[`Object.getPrototypeOf`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf +[`Object.setPrototypeOf`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf [`REPL`]: repl.html [`Writable`]: stream.html#stream_class_stream_writable [`child_process`]: child_process.html diff --git a/doc/node.1 b/doc/node.1 index c127654748302d..7219fc10c767e8 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -100,6 +100,14 @@ The default is File name of the V8 CPU profile generated with .Fl -cpu-prof . +.It Fl -disable-proto Ns = Ns Ar mode +Disable the `Object.prototype.__proto__` property. If +.Ar mode +is `delete`, the property will be removed entirely. If +.Ar mode +is `throw`, accesses to the property will throw an exception with the code +`ERR_PROTO_ACCESS`. +. .It Fl -disallow-code-generation-from-strings Make built-in language features like `eval` and `new Function` that generate code from strings throw an exception instead. This does not affect the Node.js diff --git a/src/api/environment.cc b/src/api/environment.cc index 67f7f36303924f..39eec649890971 100644 --- a/src/api/environment.cc +++ b/src/api/environment.cc @@ -14,6 +14,7 @@ using v8::Context; using v8::EscapableHandleScope; using v8::FinalizationGroup; using v8::Function; +using v8::FunctionCallbackInfo; using v8::HandleScope; using v8::Isolate; using v8::Local; @@ -23,6 +24,7 @@ using v8::Null; using v8::Object; using v8::ObjectTemplate; using v8::Private; +using v8::PropertyDescriptor; using v8::String; using v8::Value; @@ -403,6 +405,10 @@ Local NewContext(Isolate* isolate, return context; } +void ProtoThrower(const FunctionCallbackInfo& info) { + THROW_ERR_PROTO_ACCESS(info.GetIsolate()); +} + // This runs at runtime, regardless of whether the context // is created from a snapshot. void InitializeContextRuntime(Local context) { @@ -431,6 +437,32 @@ void InitializeContextRuntime(Local context) { Local atomics = atomics_v.As(); atomics->Delete(context, wake_string).FromJust(); } + + // Remove __proto__ + // https://github.com/nodejs/node/issues/31951 + Local object_string = FIXED_ONE_BYTE_STRING(isolate, "Object"); + Local prototype_string = FIXED_ONE_BYTE_STRING(isolate, "prototype"); + Local prototype = context->Global() + ->Get(context, object_string) + .ToLocalChecked() + .As() + ->Get(context, prototype_string) + .ToLocalChecked() + .As(); + Local proto_string = FIXED_ONE_BYTE_STRING(isolate, "__proto__"); + if (per_process::cli_options->disable_proto == "delete") { + prototype->Delete(context, proto_string).ToChecked(); + } else if (per_process::cli_options->disable_proto == "throw") { + Local thrower = + Function::New(context, ProtoThrower).ToLocalChecked(); + PropertyDescriptor descriptor(thrower, thrower); + descriptor.set_enumerable(false); + descriptor.set_configurable(true); + prototype->DefineProperty(context, proto_string, descriptor).ToChecked(); + } else if (per_process::cli_options->disable_proto != "") { + // Validated in ProcessGlobalArgs + FatalError("InitializeContextRuntime()", "invalid --disable-proto mode"); + } } bool InitializeContextForSnapshot(Local context) { diff --git a/src/node.cc b/src/node.cc index bd004cee8763b2..da6aebf7178cf6 100644 --- a/src/node.cc +++ b/src/node.cc @@ -670,6 +670,13 @@ int ProcessGlobalArgs(std::vector* args, } } + if (per_process::cli_options->disable_proto != "delete" && + per_process::cli_options->disable_proto != "throw" && + per_process::cli_options->disable_proto != "") { + errors->emplace_back("invalid mode passed to --disable-proto"); + return 12; + } + auto env_opts = per_process::cli_options->per_isolate->per_env; if (std::find(v8_args.begin(), v8_args.end(), "--abort-on-uncaught-exception") != v8_args.end() || diff --git a/src/node_errors.h b/src/node_errors.h index 0c4dcf63e7ef45..2c421e80721484 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -31,33 +31,34 @@ void OnFatalError(const char* location, const char* message); // `node::ERR_INVALID_ARG_TYPE(isolate, "message")` returning // a `Local` containing the TypeError with proper code and message -#define ERRORS_WITH_CODE(V) \ - V(ERR_BUFFER_CONTEXT_NOT_AVAILABLE, Error) \ - V(ERR_BUFFER_OUT_OF_BOUNDS, RangeError) \ - V(ERR_BUFFER_TOO_LARGE, Error) \ - V(ERR_CONSTRUCT_CALL_REQUIRED, TypeError) \ - V(ERR_CONSTRUCT_CALL_INVALID, TypeError) \ - V(ERR_CRYPTO_UNKNOWN_CIPHER, Error) \ - V(ERR_CRYPTO_UNKNOWN_DH_GROUP, Error) \ - V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \ - V(ERR_INVALID_ARG_VALUE, TypeError) \ - V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \ - V(ERR_INVALID_ARG_TYPE, TypeError) \ - V(ERR_INVALID_TRANSFER_OBJECT, TypeError) \ - V(ERR_MEMORY_ALLOCATION_FAILED, Error) \ - V(ERR_MISSING_ARGS, TypeError) \ - V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, TypeError) \ - V(ERR_MISSING_PASSPHRASE, TypeError) \ - V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \ - V(ERR_NON_CONTEXT_AWARE_DISABLED, Error) \ - V(ERR_OUT_OF_RANGE, RangeError) \ - V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \ - V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \ - V(ERR_STRING_TOO_LONG, Error) \ - V(ERR_TLS_INVALID_PROTOCOL_METHOD, TypeError) \ - V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, TypeError) \ - V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, Error) \ - V(ERR_VM_MODULE_CACHED_DATA_REJECTED, Error) \ +#define ERRORS_WITH_CODE(V) \ + V(ERR_BUFFER_CONTEXT_NOT_AVAILABLE, Error) \ + V(ERR_BUFFER_OUT_OF_BOUNDS, RangeError) \ + V(ERR_BUFFER_TOO_LARGE, Error) \ + V(ERR_CONSTRUCT_CALL_REQUIRED, TypeError) \ + V(ERR_CONSTRUCT_CALL_INVALID, TypeError) \ + V(ERR_CRYPTO_UNKNOWN_CIPHER, Error) \ + V(ERR_CRYPTO_UNKNOWN_DH_GROUP, Error) \ + V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \ + V(ERR_INVALID_ARG_VALUE, TypeError) \ + V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \ + V(ERR_INVALID_ARG_TYPE, TypeError) \ + V(ERR_INVALID_TRANSFER_OBJECT, TypeError) \ + V(ERR_MEMORY_ALLOCATION_FAILED, Error) \ + V(ERR_MISSING_ARGS, TypeError) \ + V(ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST, TypeError) \ + V(ERR_MISSING_PASSPHRASE, TypeError) \ + V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \ + V(ERR_NON_CONTEXT_AWARE_DISABLED, Error) \ + V(ERR_OUT_OF_RANGE, RangeError) \ + V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \ + V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \ + V(ERR_STRING_TOO_LONG, Error) \ + V(ERR_TLS_INVALID_PROTOCOL_METHOD, TypeError) \ + V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, TypeError) \ + V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, Error) \ + V(ERR_VM_MODULE_CACHED_DATA_REJECTED, Error) \ + V(ERR_PROTO_ACCESS, Error) #define V(code, type) \ inline v8::Local code(v8::Isolate* isolate, \ @@ -105,7 +106,10 @@ void OnFatalError(const char* location, const char* message); "Script execution was interrupted by `SIGINT`") \ V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, \ "Cannot serialize externalized SharedArrayBuffer") \ - V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, "Failed to set PSK identity hint") + V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, "Failed to set PSK identity hint") \ + V(ERR_PROTO_ACCESS, \ + "Accessing Object.prototype.__proto__ has been " \ + "disallowed with --disable-proto=throw") #define V(code, message) \ inline v8::Local code(v8::Isolate* isolate) { \ diff --git a/src/node_options.cc b/src/node_options.cc index 87bb407101fa50..b08060b3e5479e 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -683,7 +683,10 @@ PerProcessOptionsParser::PerProcessOptionsParser( "", /* undocumented, only for debugging */ &PerProcessOptions::debug_arraybuffer_allocations, kAllowedInEnvironment); - + AddOption("--disable-proto", + "disable Object.prototype.__proto__", + &PerProcessOptions::disable_proto, + kAllowedInEnvironment); // 12.x renamed this inadvertently, so alias it for consistency within the // release line, while using the original name for consistency with older diff --git a/src/node_options.h b/src/node_options.h index 5996e529aa3e72..46e11850e557ba 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -207,6 +207,7 @@ class PerProcessOptions : public Options { int64_t v8_thread_pool_size = 4; bool zero_fill_all_buffers = false; bool debug_arraybuffer_allocations = false; + std::string disable_proto; std::vector security_reverts; bool print_bash_completion = false; diff --git a/test/parallel/test-disable-proto-delete.js b/test/parallel/test-disable-proto-delete.js new file mode 100644 index 00000000000000..90cf9287bb9fc5 --- /dev/null +++ b/test/parallel/test-disable-proto-delete.js @@ -0,0 +1,25 @@ +// Flags: --disable-proto=delete + +'use strict'; + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const { Worker, isMainThread } = require('worker_threads'); + +// eslint-disable-next-line no-proto +assert.strictEqual(Object.prototype.__proto__, undefined); +assert(!Object.prototype.hasOwnProperty('__proto__')); + +const ctx = vm.createContext(); +const ctxGlobal = vm.runInContext('this', ctx); + +// eslint-disable-next-line no-proto +assert.strictEqual(ctxGlobal.Object.prototype.__proto__, undefined); +assert(!ctxGlobal.Object.prototype.hasOwnProperty('__proto__')); + +if (isMainThread) { + new Worker(__filename); +} else { + process.exit(); +} diff --git a/test/parallel/test-disable-proto-throw.js b/test/parallel/test-disable-proto-throw.js new file mode 100644 index 00000000000000..e7a1f679765235 --- /dev/null +++ b/test/parallel/test-disable-proto-throw.js @@ -0,0 +1,44 @@ +// Flags: --disable-proto=throw + +'use strict'; + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const { Worker, isMainThread } = require('worker_threads'); + +assert(Object.prototype.hasOwnProperty('__proto__')); + +assert.throws(() => { + // eslint-disable-next-line no-proto + ({}).__proto__; +}, { + code: 'ERR_PROTO_ACCESS' +}); + +assert.throws(() => { + // eslint-disable-next-line no-proto + ({}).__proto__ = {}; +}, { + code: 'ERR_PROTO_ACCESS', +}); + +const ctx = vm.createContext(); + +assert.throws(() => { + vm.runInContext('({}).__proto__;', ctx); +}, { + code: 'ERR_PROTO_ACCESS' +}); + +assert.throws(() => { + vm.runInContext('({}).__proto__ = {};', ctx); +}, { + code: 'ERR_PROTO_ACCESS', +}); + +if (isMainThread) { + new Worker(__filename); +} else { + process.exit(); +}