diff --git a/doc/api/cli.md b/doc/api/cli.md index 5d59844d4cbc72..7498d8eda0c8c1 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -706,7 +706,9 @@ added: Disable the `Object.prototype.__proto__` property. If `mode` is `delete`, the property is removed entirely. If `mode` is `throw`, accesses to the -property throw an exception with the code `ERR_PROTO_ACCESS`. +property throw an exception with the code `ERR_PROTO_ACCESS`. If `mode` is +`warn`, accesses to the property emit a deprecation warning while preserving +existing behavior. ### `--disable-sigusr1` diff --git a/src/api/environment.cc b/src/api/environment.cc index 3b94d860de1e79..d34ce0b6265cd9 100644 --- a/src/api/environment.cc +++ b/src/api/environment.cc @@ -788,6 +788,48 @@ void ProtoThrower(const FunctionCallbackInfo& info) { THROW_ERR_PROTO_ACCESS(info.GetIsolate()); } +void EmitProtoWarn(Isolate* isolate) { + Local context = isolate->GetCurrentContext(); + Environment* env = Environment::GetCurrent(context); + if (env->options()->pending_deprecation && env->EmitProtoWarning()) { + if (ProcessEmitDeprecationWarning( + env, + "Object.prototype.__proto__ is deprecated and will be removed in a future" + " version of Node.js. Use Object.getPrototypeOf() and " + "Object.setPrototypeOf() instead.", + "DeprecationWarning").IsNothing()) + return; + } +} + +void ProtoGetterWarn(Local property, + const PropertyCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + EmitProtoWarn(isolate); + + Local context = isolate->GetCurrentContext(); + Local receiver = info.This(); + + Local prototype; + if (!receiver->ToObject(context).ToLocalChecked() + ->GetPrototypeV2() + .ToLocal(&prototype)) { + return; + } + + info.GetReturnValue().Set(prototype); +} + +void ProtoSetterWarn(Local property, + Local value, + const PropertyCallbackInfo& info) { + Isolate* isolate = info.GetIsolate(); + EmitProtoWarn(isolate); + + Local context = isolate->GetCurrentContext(); + USE(info.This()->SetPrototypeV2(context, value)); +} + // This runs at runtime, regardless of whether the context // is created from a snapshot. Maybe InitializeContextRuntime(Local context) { @@ -861,6 +903,15 @@ Maybe InitializeContextRuntime(Local context) { .IsNothing()) { return Nothing(); } + } else if (per_process::cli_options->disable_proto == "warn") { + PropertyDescriptor descriptor(ProtoGetterWarn, ProtoSetterWarn); + descriptor.set_enumerable(false); + descriptor.set_configurable(true); + if (prototype + ->DefineProperty(context, proto_string, descriptor) + .IsNothing()) { + return Nothing(); + } } else if (per_process::cli_options->disable_proto != "") { // Validated in ProcessGlobalArgs UNREACHABLE("invalid --disable-proto mode"); diff --git a/src/env.h b/src/env.h index c2caf979023811..08577ecb62c2a3 100644 --- a/src/env.h +++ b/src/env.h @@ -934,6 +934,12 @@ class Environment final : public MemoryRetainer { return current_value; } + inline bool EmitProtoWarning() { + bool current_value = emit_proto_warning_; + emit_proto_warning_ = false; + return current_value; + } + // cb will be called as cb(env) on the next event loop iteration. // Unlike the JS setImmediate() function, nested SetImmediate() calls will // be run without returning control to the event loop, similar to nextTick(). @@ -1117,6 +1123,7 @@ class Environment final : public MemoryRetainer { bool trace_sync_io_ = false; bool emit_env_nonstring_warning_ = true; bool emit_err_name_warning_ = true; + bool emit_proto_warning_ = true; bool source_maps_enabled_ = false; size_t async_callback_scope_depth_ = 0; diff --git a/src/node.cc b/src/node.cc index b368c58734345f..94076235989a5e 100644 --- a/src/node.cc +++ b/src/node.cc @@ -753,6 +753,7 @@ static ExitCode ProcessGlobalArgsInternal(std::vector* args, if (per_process::cli_options->disable_proto != "delete" && per_process::cli_options->disable_proto != "throw" && + per_process::cli_options->disable_proto != "warn" && per_process::cli_options->disable_proto != "") { errors->emplace_back("invalid mode passed to --disable-proto"); // TODO(joyeecheung): merge into kInvalidCommandLineArgument. diff --git a/test/parallel/test-disable-proto-warn.js b/test/parallel/test-disable-proto-warn.js new file mode 100644 index 00000000000000..8ce104beb6d967 --- /dev/null +++ b/test/parallel/test-disable-proto-warn.js @@ -0,0 +1,46 @@ +// Flags: --disable-proto=warn + +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const { Worker, isMainThread } = require('worker_threads'); + +common.expectWarning( + 'DeprecationWarning', + 'Object.prototype.__proto__ is deprecated and will be removed in a future ' + + 'version of Node.js. Use Object.getPrototypeOf() and ' + + 'Object.setPrototypeOf() instead.' +); + +// eslint-disable-next-line no-proto +const proto1 = ({}).__proto__; +assert.strictEqual(proto1, Object.prototype); + +const obj = {}; +const newProto = { a: 1 }; +// eslint-disable-next-line no-proto +obj.__proto__ = newProto; +assert.strictEqual(Object.getPrototypeOf(obj), newProto); + +const ctx = vm.createContext(); +assert.strictEqual( + vm.runInContext('({}).__proto__ === Object.prototype', ctx), + true +); +assert.strictEqual( + vm.runInContext(` + const obj = {}; + const newProto = { a: 1 }; + obj.__proto__ = newProto; + Object.getPrototypeOf(obj) === newProto; + `, ctx), + true +); + +if (isMainThread) { + new Worker(__filename); +} else { + process.exit(); +}