Skip to content

Commit

Permalink
assert: enforce type check in deepStrictEqual
Browse files Browse the repository at this point in the history
Add checks for the built-in type tags to catch objects
with faked prototypes.

See https://tc39.github.io/ecma262/#sec-object.prototype.tostring
for a partial list of built-in tags.

Fixes: nodejs#10258
  • Loading branch information
joyeecheung committed Mar 11, 2017
1 parent c672077 commit 3efc514
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 1 deletion.
22 changes: 21 additions & 1 deletion doc/api/assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,13 @@ changes:
* `expected` {any}
* `message` {any}

Generally identical to `assert.deepEqual()` with two exceptions:
Generally identical to `assert.deepEqual()` with three exceptions:

1. Primitive values are compared using the [Strict Equality Comparison][]
( `===` ).
2. [`[[Prototype]]`][prototype-spec] of objects are compared using
the [Strict Equality Comparison][] too.
3. [Type tags][Object.prototype.toString()] of objects should be the same.

```js
const assert = require('assert');
Expand All @@ -141,6 +142,25 @@ assert.deepEqual({a: 1}, {a: '1'});
assert.deepStrictEqual({a: 1}, {a: '1'});
// AssertionError: { a: 1 } deepStrictEqual { a: '1' }
// because 1 !== '1' using strict equality

// The following objects don't have owned properties
const date = new Date();
const object = {};
const fakeDate = {};

Object.setPrototypeOf(fakeDate, Date.prototype);

assert.deepEqual(object, fakeDate);
// OK, doesn't check [[Prototype]]
assert.deepStrictEqual(object, fakeDate);
// AssertionError: {} deepStrictEqual Date {}
// Different [[Prototype]]

assert.deepEqual(date, fakeDate);
// OK, doesn't check type tags
assert.deepStrictEqual(date, fakeDate);
// AssertionError: 2017-03-11T14:25:31.849Z deepStrictEqual Date {}
// Different type tags
```

If the values are not equal, an `AssertionError` is thrown with a `message`
Expand Down
4 changes: 4 additions & 0 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ function _deepEqual(actual, expected, strict, memos) {
if (Object.getPrototypeOf(actual) !== Object.getPrototypeOf(expected)) {
return false;
}

if (actualTag !== expectedTag) {
return false;
}
}

// Do fast checks for builtin types.
Expand Down
61 changes: 61 additions & 0 deletions test/parallel/test-assert-checktag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict';
require('../common');
const assert = require('assert');
const util = require('util');

// Template tag function turning an error message into a RegExp
// for assert.throws()
function re(literals, ...values) {
let result = literals[0];
for (const [i, value] of values.entries()) {
const str = util.inspect(value);
// Need to escape special characters.
result += str.replace(/[\\^$.*+?()[\]{}|=!<>:-]/g, '\\$&');
result += literals[i + 1];
}
return new RegExp('^AssertionError: ' + result + '$');
}

// Turn off no-restricted-properties because we are testing deepEqual!
/* eslint-disable no-restricted-properties */

// See https://github.com/nodejs/node/issues/10258
{
const date = new Date('2016');
function FakeDate() {}
FakeDate.prototype = Date.prototype;
const fake = new FakeDate();

assert.doesNotThrow(() => assert.deepEqual(date, fake));
assert.doesNotThrow(() => assert.deepEqual(fake, date));

// For deepStrictEqual we check the runtime type,
// then reveal the fakeness of the fake date
assert.throws(() => assert.deepStrictEqual(date, fake),
re`${date} deepStrictEqual Date {}`);
assert.throws(() => assert.deepStrictEqual(fake, date),
re`Date {} deepStrictEqual ${date}`);
}

{ // At the moment global has its own type tag
const fakeGlobal = {};
Object.setPrototypeOf(fakeGlobal, Object.getPrototypeOf(global));
for (const prop of Object.keys(global)) {
fakeGlobal[prop] = global[prop];
}
assert.doesNotThrow(() => assert.deepEqual(fakeGlobal, global));
// Message will be truncated anyway, don't validate
assert.throws(() => assert.deepStrictEqual(fakeGlobal, global));
}

{ // At the moment process has its own type tag
const fakeProcess = {};
Object.setPrototypeOf(fakeProcess, Object.getPrototypeOf(process));
for (const prop of Object.keys(process)) {
fakeProcess[prop] = process[prop];
}
assert.doesNotThrow(() => assert.deepEqual(fakeProcess, process));
// Message will be truncated anyway, don't validate
assert.throws(() => assert.deepStrictEqual(fakeProcess, process));
}
/* eslint-enable */

0 comments on commit 3efc514

Please sign in to comment.