Skip to content

Commit e1e669b

Browse files
committed
url: throw invalid this on detached accessors
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 <[email protected]> PR-URL: #39752 Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Zijian Liu <[email protected]> Reviewed-By: Luigi Pinca <[email protected]>
1 parent 75bd4da commit e1e669b

File tree

2 files changed

+97
-0
lines changed

2 files changed

+97
-0
lines changed

lib/internal/url.js

+52
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,10 @@ function onParseHashComplete(flags, protocol, username, password,
625625
this[context].fragment = fragment;
626626
}
627627

628+
function isURLThis(self) {
629+
return self?.[context] !== undefined;
630+
}
631+
628632
class URL {
629633
constructor(input, base) {
630634
// toUSVString is not needed.
@@ -737,14 +741,20 @@ class URL {
737741

738742
// https://heycam.github.io/webidl/#es-stringifier
739743
toString() {
744+
if (!isURLThis(this))
745+
throw new ERR_INVALID_THIS('URL');
740746
return this[kFormat]({});
741747
}
742748

743749
get href() {
750+
if (!isURLThis(this))
751+
throw new ERR_INVALID_THIS('URL');
744752
return this[kFormat]({});
745753
}
746754

747755
set href(input) {
756+
if (!isURLThis(this))
757+
throw new ERR_INVALID_THIS('URL');
748758
// toUSVString is not needed.
749759
input = `${input}`;
750760
parse(input, -1, undefined, undefined,
@@ -753,6 +763,8 @@ class URL {
753763

754764
// readonly
755765
get origin() {
766+
if (!isURLThis(this))
767+
throw new ERR_INVALID_THIS('URL');
756768
// Refs: https://url.spec.whatwg.org/#concept-url-origin
757769
const ctx = this[context];
758770
switch (ctx.scheme) {
@@ -776,10 +788,14 @@ class URL {
776788
}
777789

778790
get protocol() {
791+
if (!isURLThis(this))
792+
throw new ERR_INVALID_THIS('URL');
779793
return this[context].scheme;
780794
}
781795

782796
set protocol(scheme) {
797+
if (!isURLThis(this))
798+
throw new ERR_INVALID_THIS('URL');
783799
// toUSVString is not needed.
784800
scheme = `${scheme}`;
785801
if (scheme.length === 0)
@@ -790,10 +806,14 @@ class URL {
790806
}
791807

792808
get username() {
809+
if (!isURLThis(this))
810+
throw new ERR_INVALID_THIS('URL');
793811
return this[context].username;
794812
}
795813

796814
set username(username) {
815+
if (!isURLThis(this))
816+
throw new ERR_INVALID_THIS('URL');
797817
// toUSVString is not needed.
798818
username = `${username}`;
799819
if (this[cannotHaveUsernamePasswordPort])
@@ -809,10 +829,14 @@ class URL {
809829
}
810830

811831
get password() {
832+
if (!isURLThis(this))
833+
throw new ERR_INVALID_THIS('URL');
812834
return this[context].password;
813835
}
814836

815837
set password(password) {
838+
if (!isURLThis(this))
839+
throw new ERR_INVALID_THIS('URL');
816840
// toUSVString is not needed.
817841
password = `${password}`;
818842
if (this[cannotHaveUsernamePasswordPort])
@@ -828,6 +852,8 @@ class URL {
828852
}
829853

830854
get host() {
855+
if (!isURLThis(this))
856+
throw new ERR_INVALID_THIS('URL');
831857
const ctx = this[context];
832858
let ret = ctx.host || '';
833859
if (ctx.port !== null)
@@ -836,6 +862,8 @@ class URL {
836862
}
837863

838864
set host(host) {
865+
if (!isURLThis(this))
866+
throw new ERR_INVALID_THIS('URL');
839867
const ctx = this[context];
840868
// toUSVString is not needed.
841869
host = `${host}`;
@@ -848,10 +876,14 @@ class URL {
848876
}
849877

850878
get hostname() {
879+
if (!isURLThis(this))
880+
throw new ERR_INVALID_THIS('URL');
851881
return this[context].host || '';
852882
}
853883

854884
set hostname(host) {
885+
if (!isURLThis(this))
886+
throw new ERR_INVALID_THIS('URL');
855887
const ctx = this[context];
856888
// toUSVString is not needed.
857889
host = `${host}`;
@@ -863,11 +895,15 @@ class URL {
863895
}
864896

865897
get port() {
898+
if (!isURLThis(this))
899+
throw new ERR_INVALID_THIS('URL');
866900
const port = this[context].port;
867901
return port === null ? '' : String(port);
868902
}
869903

870904
set port(port) {
905+
if (!isURLThis(this))
906+
throw new ERR_INVALID_THIS('URL');
871907
// toUSVString is not needed.
872908
port = `${port}`;
873909
if (this[cannotHaveUsernamePasswordPort])
@@ -882,6 +918,8 @@ class URL {
882918
}
883919

884920
get pathname() {
921+
if (!isURLThis(this))
922+
throw new ERR_INVALID_THIS('URL');
885923
const ctx = this[context];
886924
if (this[cannotBeBase])
887925
return ctx.path[0];
@@ -891,6 +929,8 @@ class URL {
891929
}
892930

893931
set pathname(path) {
932+
if (!isURLThis(this))
933+
throw new ERR_INVALID_THIS('URL');
894934
// toUSVString is not needed.
895935
path = `${path}`;
896936
if (this[cannotBeBase])
@@ -900,13 +940,17 @@ class URL {
900940
}
901941

902942
get search() {
943+
if (!isURLThis(this))
944+
throw new ERR_INVALID_THIS('URL');
903945
const { query } = this[context];
904946
if (query === null || query === '')
905947
return '';
906948
return `?${query}`;
907949
}
908950

909951
set search(search) {
952+
if (!isURLThis(this))
953+
throw new ERR_INVALID_THIS('URL');
910954
const ctx = this[context];
911955
search = toUSVString(search);
912956
if (search === '') {
@@ -926,17 +970,23 @@ class URL {
926970

927971
// readonly
928972
get searchParams() {
973+
if (!isURLThis(this))
974+
throw new ERR_INVALID_THIS('URL');
929975
return this[searchParams];
930976
}
931977

932978
get hash() {
979+
if (!isURLThis(this))
980+
throw new ERR_INVALID_THIS('URL');
933981
const { fragment } = this[context];
934982
if (fragment === null || fragment === '')
935983
return '';
936984
return `#${fragment}`;
937985
}
938986

939987
set hash(hash) {
988+
if (!isURLThis(this))
989+
throw new ERR_INVALID_THIS('URL');
940990
const ctx = this[context];
941991
// toUSVString is not needed.
942992
hash = `${hash}`;
@@ -953,6 +1003,8 @@ class URL {
9531003
}
9541004

9551005
toJSON() {
1006+
if (!isURLThis(this))
1007+
throw new ERR_INVALID_THIS('URL');
9561008
return this[kFormat]({});
9571009
}
9581010

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const { URL } = require('url');
6+
const assert = require('assert');
7+
8+
[
9+
'toString',
10+
'toJSON',
11+
].forEach((i) => {
12+
assert.throws(() => Reflect.apply(URL.prototype[i], [], {}), {
13+
code: 'ERR_INVALID_THIS',
14+
});
15+
});
16+
17+
[
18+
'href',
19+
'protocol',
20+
'username',
21+
'password',
22+
'host',
23+
'hostname',
24+
'port',
25+
'pathname',
26+
'search',
27+
'hash',
28+
].forEach((i) => {
29+
assert.throws(() => Reflect.get(URL.prototype, i, {}), {
30+
code: 'ERR_INVALID_THIS',
31+
});
32+
33+
assert.throws(() => Reflect.set(URL.prototype, i, null, {}), {
34+
code: 'ERR_INVALID_THIS',
35+
});
36+
});
37+
38+
[
39+
'origin',
40+
'searchParams',
41+
].forEach((i) => {
42+
assert.throws(() => Reflect.get(URL.prototype, i, {}), {
43+
code: 'ERR_INVALID_THIS',
44+
});
45+
});

0 commit comments

Comments
 (0)