From e1e669b1095fdb9d89fbb58310c2500fb7a78dbc Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 12 Aug 2021 15:45:54 -0700 Subject: [PATCH] url: throw invalid this on detached accessors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, using Reflect.get/set or calling a member method like toString() detached from the instance would result in an obscure internal error. This adds a proper brand check and throws `ERR_INVALID_THIS` when appropriate. Signed-off-by: James M Snell PR-URL: https://github.com/nodejs/node/pull/39752 Reviewed-By: Michaƫl Zasso Reviewed-By: Antoine du Hamel Reviewed-By: Zijian Liu Reviewed-By: Luigi Pinca --- lib/internal/url.js | 52 ++++++++++++++++++++ test/parallel/test-whatwg-url-invalidthis.js | 45 +++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 test/parallel/test-whatwg-url-invalidthis.js diff --git a/lib/internal/url.js b/lib/internal/url.js index 378248b9486b52..9cb14654381e48 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -625,6 +625,10 @@ function onParseHashComplete(flags, protocol, username, password, this[context].fragment = fragment; } +function isURLThis(self) { + return self?.[context] !== undefined; +} + class URL { constructor(input, base) { // toUSVString is not needed. @@ -737,14 +741,20 @@ class URL { // https://heycam.github.io/webidl/#es-stringifier toString() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); return this[kFormat]({}); } get href() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); return this[kFormat]({}); } set href(input) { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); // toUSVString is not needed. input = `${input}`; parse(input, -1, undefined, undefined, @@ -753,6 +763,8 @@ class URL { // readonly get origin() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); // Refs: https://url.spec.whatwg.org/#concept-url-origin const ctx = this[context]; switch (ctx.scheme) { @@ -776,10 +788,14 @@ class URL { } get protocol() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); return this[context].scheme; } set protocol(scheme) { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); // toUSVString is not needed. scheme = `${scheme}`; if (scheme.length === 0) @@ -790,10 +806,14 @@ class URL { } get username() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); return this[context].username; } set username(username) { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); // toUSVString is not needed. username = `${username}`; if (this[cannotHaveUsernamePasswordPort]) @@ -809,10 +829,14 @@ class URL { } get password() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); return this[context].password; } set password(password) { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); // toUSVString is not needed. password = `${password}`; if (this[cannotHaveUsernamePasswordPort]) @@ -828,6 +852,8 @@ class URL { } get host() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); const ctx = this[context]; let ret = ctx.host || ''; if (ctx.port !== null) @@ -836,6 +862,8 @@ class URL { } set host(host) { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); const ctx = this[context]; // toUSVString is not needed. host = `${host}`; @@ -848,10 +876,14 @@ class URL { } get hostname() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); return this[context].host || ''; } set hostname(host) { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); const ctx = this[context]; // toUSVString is not needed. host = `${host}`; @@ -863,11 +895,15 @@ class URL { } get port() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); const port = this[context].port; return port === null ? '' : String(port); } set port(port) { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); // toUSVString is not needed. port = `${port}`; if (this[cannotHaveUsernamePasswordPort]) @@ -882,6 +918,8 @@ class URL { } get pathname() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); const ctx = this[context]; if (this[cannotBeBase]) return ctx.path[0]; @@ -891,6 +929,8 @@ class URL { } set pathname(path) { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); // toUSVString is not needed. path = `${path}`; if (this[cannotBeBase]) @@ -900,6 +940,8 @@ class URL { } get search() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); const { query } = this[context]; if (query === null || query === '') return ''; @@ -907,6 +949,8 @@ class URL { } set search(search) { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); const ctx = this[context]; search = toUSVString(search); if (search === '') { @@ -926,10 +970,14 @@ class URL { // readonly get searchParams() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); return this[searchParams]; } get hash() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); const { fragment } = this[context]; if (fragment === null || fragment === '') return ''; @@ -937,6 +985,8 @@ class URL { } set hash(hash) { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); const ctx = this[context]; // toUSVString is not needed. hash = `${hash}`; @@ -953,6 +1003,8 @@ class URL { } toJSON() { + if (!isURLThis(this)) + throw new ERR_INVALID_THIS('URL'); return this[kFormat]({}); } diff --git a/test/parallel/test-whatwg-url-invalidthis.js b/test/parallel/test-whatwg-url-invalidthis.js new file mode 100644 index 00000000000000..790c28e37c13ed --- /dev/null +++ b/test/parallel/test-whatwg-url-invalidthis.js @@ -0,0 +1,45 @@ +'use strict'; + +require('../common'); + +const { URL } = require('url'); +const assert = require('assert'); + +[ + 'toString', + 'toJSON', +].forEach((i) => { + assert.throws(() => Reflect.apply(URL.prototype[i], [], {}), { + code: 'ERR_INVALID_THIS', + }); +}); + +[ + 'href', + 'protocol', + 'username', + 'password', + 'host', + 'hostname', + 'port', + 'pathname', + 'search', + 'hash', +].forEach((i) => { + assert.throws(() => Reflect.get(URL.prototype, i, {}), { + code: 'ERR_INVALID_THIS', + }); + + assert.throws(() => Reflect.set(URL.prototype, i, null, {}), { + code: 'ERR_INVALID_THIS', + }); +}); + +[ + 'origin', + 'searchParams', +].forEach((i) => { + assert.throws(() => Reflect.get(URL.prototype, i, {}), { + code: 'ERR_INVALID_THIS', + }); +});