diff --git c/src/match.js i/src/match.js index de61470..2ac10d1 100644 --- c/src/match.js +++ i/src/match.js @@ -5,6 +5,7 @@ const DataStream = require('./datastream') const events = require('./events') const Hoopy = require('hoopy') const parseJsonPath = require('./jsonpath') +const setProperty = require('./set-property') const { PassThrough } = require('node:stream') const walk = require('./walk') @@ -225,7 +226,7 @@ function match (stream, selector, options = {}) { key = properties.pop() } - scope[key] = v + setProperty(scope, key, v) } if (v === null) { diff --git c/src/parse.js i/src/parse.js index f787289..b56627b 100644 --- c/src/parse.js +++ i/src/parse.js @@ -2,6 +2,7 @@ const check = require('check-types') const events = require('./events') +const setProperty = require('./set-property') const walk = require('./walk') module.exports = parse @@ -99,7 +100,7 @@ async function parse (stream, options = {}) { const scope = scopes[scopes.length - 1] if (scopeKey) { - scope[scopeKey] = v + setProperty(scope, scopeKey, v) scopeKey = null } else { scope.push(v) diff --git c/src/set-property.js i/src/set-property.js new file mode 100644 index 0000000..0136455 --- /dev/null +++ i/src/set-property.js @@ -0,0 +1,27 @@ +'use strict' + +module.exports = setProperty + +/** + * Set an own data property on an object. + * + * A `__proto__` key must be defined rather than assigned, otherwise it mutates + * the object's prototype instead of creating the property that the JSON input + * represents. + * + * @param object: The object to set the property on. + * @param key: The property key. + * @param value: The property value. + **/ +function setProperty (object, key, value) { + if (key === '__proto__') { + Object.defineProperty(object, key, { + value, + writable: true, + enumerable: true, + configurable: true + }) + } else { + object[key] = value + } +} diff --git c/test/integration.js i/test/integration.js index 1ab73e8..7453029 100644 --- c/test/integration.js +++ i/test/integration.js @@ -408,6 +408,63 @@ suite('integration:', () => { }) }) + suite('__proto__ prototype injection in parse:', () => { + const payload = '{"username":"attacker","__proto__":{"isAdmin":true}}' + + let parsed, reviverKeys + + setup(async () => { + reviverKeys = [] + parsed = await bfj.parse(stream.Readable.from([ payload ]), { + reviver: (key, value) => { + reviverKeys.push(key) + return key === 'isAdmin' ? undefined : value + }, + }) + }) + + test('the prototype was not changed', () => { + assert.strictEqual(Object.getPrototypeOf(parsed), Object.prototype) + }) + + test('__proto__ is an own property', () => { + assert.include(Object.keys(parsed), '__proto__') + }) + + test('the reviver saw the injected property', () => { + assert.include(reviverKeys, 'isAdmin') + }) + + test('the reviver removed the injected property', () => { + assert.isUndefined(parsed.isAdmin) + }) + }) + + suite('__proto__ prototype injection in match:', () => { + const payload = '{"username":"attacker","__proto__":{"isAdmin":true}}' + + let results, errors + + setup(done => { + results = [] + errors = [] + const datastream = bfj.match(stream.Readable.from([ payload ]), () => true) + datastream.on('data', item => results.push(item)) + datastream.on('error', error => errors.push(error)) + datastream.on('end', done) + }) + + test('no errors occurred', () => { + assert.deepEqual(errors, []) + }) + + test('the matched object was not prototype-injected', () => { + const object = results.find(result => result && result.username === 'attacker') + assert.isObject(object) + assert.strictEqual(Object.getPrototypeOf(object), Object.prototype) + }) + }) + suite('stringify value:', () => { let result diff --git c/test/unit/set-property.js i/test/unit/set-property.js new file mode 100644 index 0000000..42f48d9 --- /dev/null +++ i/test/unit/set-property.js @@ -0,0 +1,118 @@ +'use strict' + +const assert = require('chai').assert +const modulePath = '../../src/set-property' + +suite('set-property:', () => { + test('require does not throw', () => { + assert.doesNotThrow(() => { + require(modulePath) + }) + }) + + test('require returns function', () => { + assert.isFunction(require(modulePath)) + }) + + suite('require:', () => { + let setProperty + + setup(() => { + setProperty = require(modulePath) + }) + + test('setProperty expects three arguments', () => { + assert.lengthOf(setProperty, 3) + }) + + suite('set regular property:', () => { + let object + + setup(() => { + object = {} + setProperty(object, 'foo', 'bar') + }) + + test('property was set', () => { + assert.strictEqual(object.foo, 'bar') + }) + + test('property is own', () => { + assert.isTrue(Object.prototype.hasOwnProperty.call(object, 'foo')) + }) + + test('property is enumerable', () => { + assert.deepEqual(Object.keys(object), [ 'foo' ]) + }) + + test('prototype was not changed', () => { + assert.strictEqual(Object.getPrototypeOf(object), Object.prototype) + }) + }) + + suite('set __proto__ property:', () => { + let object + + setup(() => { + object = {} + setProperty(object, '__proto__', { foo: 'bar' }) + }) + + test('property is own', () => { + assert.isTrue(Object.prototype.hasOwnProperty.call(object, '__proto__')) + }) + + test('property is enumerable', () => { + assert.deepEqual(Object.keys(object), [ '__proto__' ]) + }) + + test('property value was set', () => { + assert.deepEqual(object['__proto__'], { foo: 'bar' }) + }) + + test('prototype was not changed', () => { + assert.strictEqual(Object.getPrototypeOf(object), Object.prototype) + }) + + test('property value is not inherited', () => { + assert.isUndefined(object.foo) + }) + + test('property is writable', () => { + object['__proto__'] = 'baz' + assert.strictEqual(object['__proto__'], 'baz') + }) + + test('prototype was still not changed after assignment', () => { + object['__proto__'] = 'baz' + assert.strictEqual(Object.getPrototypeOf(object), Object.prototype) + }) + + test('property is configurable', () => { + delete object['__proto__'] + assert.deepEqual(Object.keys(object), []) + }) + }) + + suite('set __proto__ property to null:', () => { + let object + + setup(() => { + object = {} + setProperty(object, '__proto__', null) + }) + + test('property is own', () => { + assert.isTrue(Object.prototype.hasOwnProperty.call(object, '__proto__')) + }) + + test('property value was set', () => { + assert.isNull(object['__proto__']) + }) + + test('prototype was not changed', () => { + assert.strictEqual(Object.getPrototypeOf(object), Object.prototype) + }) + }) + }) +})