diff --git a/README.md b/README.md index ce33fde..8e5a832 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ options: - `forceFloat64`, a boolean to that forces all floats to be encoded as 64-bits floats. Defaults to false. - `compatibilityMode`, a boolean that enables "compatibility mode" which doesn't use str 8 format. Defaults to false. +- `protoAction`, a string which can be `error|ignore|remove` that determines what happens when decoding a plain object with a `__proto__` property which would cause prototype poisoning. `error` (default) throws an error, `remove` removes the property, `ignore` (not recommended) allows the property, thereby causing prototype poisoning on the decoded object. ------------------------------------------------------- diff --git a/index.js b/index.js index 4c7d7d9..679c800 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,9 @@ function msgpack (options) { options = options || { forceFloat64: false, - compatibilityMode: false + compatibilityMode: false, + // options.protoAction: 'error' (default) / 'remove' / 'ignore' + protoAction: 'error' } function registerEncoder (check, encode) { @@ -68,7 +70,7 @@ function msgpack (options) { return { encode: buildEncode(encodingTypes, options.forceFloat64, options.compatibilityMode), - decode: buildDecode(decodingTypes), + decode: buildDecode(decodingTypes, options), register: register, registerEncoder: registerEncoder, registerDecoder: registerDecoder, diff --git a/lib/decoder.js b/lib/decoder.js index 86b63b5..c83b784 100644 --- a/lib/decoder.js +++ b/lib/decoder.js @@ -12,7 +12,7 @@ function IncompleteBufferError (message) { util.inherits(IncompleteBufferError, Error) -module.exports = function buildDecode (decodingTypes) { +module.exports = function buildDecode (decodingTypes, options) { return decode function getSize (first) { @@ -358,6 +358,17 @@ module.exports = function buildDecode (decodingTypes) { var valueResult = tryDecode(buf, offset) if (valueResult) { key = keyResult.value + + if (key === '__proto__') { + if (options.protoAction === 'error') { + throw new SyntaxError('Object contains forbidden prototype property') + } + + if (options.protoAction === 'remove') { + continue + } + } + result[key] = valueResult.value offset += valueResult.bytesConsumed totalBytesConsumed += (keyResult.bytesConsumed + valueResult.bytesConsumed) diff --git a/test/object-prototype-poisoning.js b/test/object-prototype-poisoning.js new file mode 100644 index 0000000..18644cd --- /dev/null +++ b/test/object-prototype-poisoning.js @@ -0,0 +1,49 @@ +'use strict' + +var test = require('tape').test +var msgpack = require('../') + +test('decode throws when object has forbidden __proto__ property', function (t) { + const encoder = msgpack() + + const payload = { hello: 'world' } + Object.defineProperty(payload, '__proto__', { + value: { polluted: true }, + enumerable: true + }) + + const encoded = encoder.encode(payload) + + t.throws(() => encoder.decode(encoded), /Object contains forbidden prototype property/) + t.end() +}) + +test('decode ignores forbidden __proto__ property if protoAction is "ignore"', function (t) { + const encoder = msgpack({ protoAction: 'ignore' }) + + const payload = { hello: 'world' } + Object.defineProperty(payload, '__proto__', { + value: { polluted: true }, + enumerable: true + }) + + const decoded = encoder.decode(encoder.encode(payload)) + + t.equal(decoded.polluted, true) + t.end() +}) + +test('decode removes forbidden __proto__ property if protoAction is "remove"', function (t) { + const encoder = msgpack({ protoAction: 'remove' }) + + const payload = { hello: 'world' } + Object.defineProperty(payload, '__proto__', { + value: { polluted: true }, + enumerable: true + }) + + const decoded = encoder.decode(encoder.encode(payload)) + + t.equal(decoded.polluted, undefined) + t.end() +})