From 2f3ade7a0bbb315d467141409bc956fa8742ab3f Mon Sep 17 00:00:00 2001 From: Simone Busoli Date: Mon, 8 Mar 2021 18:32:36 +0100 Subject: [PATCH] fix: prevent object prototype poisoning (#99) --- README.md | 1 + index.js | 6 ++-- lib/decoder.js | 13 +++++++- test/object-prototype-poisoning.js | 49 ++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 test/object-prototype-poisoning.js diff --git a/README.md b/README.md index e246d23..b265d75 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,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. - `disableTimestampEncoding`, a boolean that when set disables the encoding of Dates into the [timestamp extension type](https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type). 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 d65e93d..1cfc278 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,9 @@ function msgpack (options) { options = options || { forceFloat64: false, compatibilityMode: false, - disableTimestampEncoding: false // if true, skips encoding Dates using the msgpack timestamp ext format (-1) + disableTimestampEncoding: false, // if true, skips encoding Dates using the msgpack timestamp ext format (-1) + // options.protoAction: 'error' (default) / 'remove' / 'ignore' + protoAction: 'error' } function registerEncoder (check, encode) { @@ -69,7 +71,7 @@ function msgpack (options) { return { encode: buildEncode(encodingTypes, options.forceFloat64, options.compatibilityMode, options.disableTimestampEncoding), - decode: buildDecode(decodingTypes), + decode: buildDecode(decodingTypes, options), register: register, registerEncoder: registerEncoder, registerDecoder: registerDecoder, diff --git a/lib/decoder.js b/lib/decoder.js index 1edc6e5..ca0581b 100644 --- a/lib/decoder.js +++ b/lib/decoder.js @@ -14,7 +14,7 @@ function IncompleteBufferError (message) { util.inherits(IncompleteBufferError, Error) -module.exports = function buildDecode (decodingTypes) { +module.exports = function buildDecode (decodingTypes, options) { return decode function getSize (first) { @@ -361,6 +361,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() +})