Skip to content

Commit bf4faf3

Browse files
BridgeARBethGriggs
authored andcommitted
assert,util: harden comparison
The former algorithm used checks which were unsafe. Most of these have been replaced with alternatives that can not be manipulated or fooled that easily. PR-URL: #24831 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Ujjwal Sharma <usharma1998@gmail.com>
1 parent 25dae6c commit bf4faf3

File tree

2 files changed

+102
-13
lines changed

2 files changed

+102
-13
lines changed

lib/internal/util/comparisons.js

+38-13
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ const {
77
isDate,
88
isMap,
99
isRegExp,
10-
isSet
10+
isSet,
11+
isNativeError,
12+
isBoxedPrimitive,
13+
isNumberObject,
14+
isStringObject,
15+
isBooleanObject,
16+
isBigIntObject,
17+
isSymbolObject
1118
} = internalBinding('types');
1219
const {
1320
getOwnNonIndexProperties,
@@ -33,6 +40,13 @@ const kIsMap = 3;
3340
const objectToString = uncurryThis(Object.prototype.toString);
3441
const hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty);
3542
const propertyIsEnumerable = uncurryThis(Object.prototype.propertyIsEnumerable);
43+
const dateGetTime = uncurryThis(Date.prototype.getTime);
44+
45+
const bigIntValueOf = uncurryThis(BigInt.prototype.valueOf);
46+
const booleanValueOf = uncurryThis(Boolean.prototype.valueOf);
47+
const numberValueOf = uncurryThis(Number.prototype.valueOf);
48+
const symbolValueOf = uncurryThis(Symbol.prototype.valueOf);
49+
const stringValueOf = uncurryThis(String.prototype.valueOf);
3650

3751
const objectKeys = Object.keys;
3852
const getPrototypeOf = Object.getPrototypeOf;
@@ -82,6 +96,24 @@ function isObjectOrArrayTag(tag) {
8296
return tag === '[object Array]' || tag === '[object Object]';
8397
}
8498

99+
function isEqualBoxedPrimitive(val1, val2) {
100+
if (isNumberObject(val1)) {
101+
return isNumberObject(val2) &&
102+
objectIs(numberValueOf(val1), numberValueOf(val2));
103+
}
104+
if (isStringObject(val1)) {
105+
return isStringObject(val2) && stringValueOf(val1) === stringValueOf(val2);
106+
}
107+
if (isBooleanObject(val1)) {
108+
return isBooleanObject(val2) &&
109+
booleanValueOf(val1) === booleanValueOf(val2);
110+
}
111+
if (isBigIntObject(val1)) {
112+
return isBigIntObject(val2) && bigIntValueOf(val1) === bigIntValueOf(val2);
113+
}
114+
return isSymbolObject(val2) && symbolValueOf(val1) === symbolValueOf(val2);
115+
}
116+
85117
// Notes: Type tags are historical [[Class]] properties that can be set by
86118
// FunctionTemplate::SetClassName() in C++ or Symbol.toStringTag in JS
87119
// and retrieved using Object.prototype.toString.call(obj) in JS
@@ -117,7 +149,7 @@ function strictDeepEqual(val1, val2, memos) {
117149
if (getPrototypeOf(val1) !== getPrototypeOf(val2)) {
118150
return false;
119151
}
120-
if (val1Tag === '[object Array]') {
152+
if (Array.isArray(val1)) {
121153
// Check for sparse arrays and general fast path
122154
if (val1.length !== val2.length) {
123155
return false;
@@ -133,15 +165,14 @@ function strictDeepEqual(val1, val2, memos) {
133165
return keyCheck(val1, val2, kStrict, memos, kNoIterator);
134166
}
135167
if (isDate(val1)) {
136-
// TODO: Make these safe.
137-
if (val1.getTime() !== val2.getTime()) {
168+
if (dateGetTime(val1) !== dateGetTime(val2)) {
138169
return false;
139170
}
140171
} else if (isRegExp(val1)) {
141172
if (!areSimilarRegExps(val1, val2)) {
142173
return false;
143174
}
144-
} else if (val1Tag === '[object Error]') {
175+
} else if (isNativeError(val1) || val1 instanceof Error) {
145176
// Do not compare the stack as it might differ even though the error itself
146177
// is otherwise identical. The non-enumerable name should be identical as
147178
// the prototype is also identical. Otherwise this is caught later on.
@@ -175,14 +206,8 @@ function strictDeepEqual(val1, val2, memos) {
175206
if (!areEqualArrayBuffers(val1, val2)) {
176207
return false;
177208
}
178-
// TODO: Make the valueOf checks safe.
179-
} else if (typeof val1.valueOf === 'function') {
180-
const val1Value = val1.valueOf();
181-
if (val1Value !== val1 &&
182-
(typeof val2.valueOf !== 'function' ||
183-
!innerDeepEqual(val1Value, val2.valueOf(), kStrict))) {
184-
return false;
185-
}
209+
} else if (isBoxedPrimitive(val1) && !isEqualBoxedPrimitive(val1, val2)) {
210+
return false;
186211
}
187212
return keyCheck(val1, val2, kStrict, memos, kNoIterator);
188213
}

test/parallel/test-assert-deep.js

+64
Original file line numberDiff line numberDiff line change
@@ -985,3 +985,67 @@ assert.throws(
985985
' ]'
986986
}
987987
);
988+
989+
// Verify that manipulating the `getTime()` function has no impact on the time
990+
// verification.
991+
{
992+
const a = new Date('2000');
993+
const b = new Date('2000');
994+
Object.defineProperty(a, 'getTime', {
995+
value: () => 5
996+
});
997+
assert.deepStrictEqual(a, b);
998+
}
999+
1000+
// Verify that extra keys will be tested for when using fake arrays.
1001+
{
1002+
const a = {
1003+
0: 1,
1004+
1: 1,
1005+
2: 'broken'
1006+
};
1007+
Object.setPrototypeOf(a, Object.getPrototypeOf([]));
1008+
Object.defineProperty(a, Symbol.toStringTag, {
1009+
value: 'Array',
1010+
});
1011+
Object.defineProperty(a, 'length', {
1012+
value: 2
1013+
});
1014+
assert.notDeepStrictEqual(a, [1, 1]);
1015+
}
1016+
1017+
// Verify that changed tags will still check for the error message.
1018+
{
1019+
const err = new Error('foo');
1020+
err[Symbol.toStringTag] = 'Foobar';
1021+
const err2 = new Error('bar');
1022+
err2[Symbol.toStringTag] = 'Foobar';
1023+
assertNotDeepOrStrict(err, err2, AssertionError);
1024+
}
1025+
1026+
// Check for non-native errors.
1027+
{
1028+
const source = new Error('abc');
1029+
const err = Object.create(
1030+
Object.getPrototypeOf(source), Object.getOwnPropertyDescriptors(source));
1031+
Object.defineProperty(err, 'message', { value: 'foo' });
1032+
const err2 = Object.create(
1033+
Object.getPrototypeOf(source), Object.getOwnPropertyDescriptors(source));
1034+
Object.defineProperty(err2, 'message', { value: 'bar' });
1035+
err[Symbol.toStringTag] = 'Foo';
1036+
err2[Symbol.toStringTag] = 'Foo';
1037+
assert.notDeepStrictEqual(err, err2);
1038+
}
1039+
1040+
// Verify that `valueOf` is not called for boxed primitives.
1041+
{
1042+
const a = new Number(5);
1043+
const b = new Number(5);
1044+
Object.defineProperty(a, 'valueOf', {
1045+
value: () => { throw new Error('failed'); }
1046+
});
1047+
Object.defineProperty(b, 'valueOf', {
1048+
value: () => { throw new Error('failed'); }
1049+
});
1050+
assertDeepAndStrictEqual(a, b);
1051+
}

0 commit comments

Comments
 (0)