CVE-2020-7660 (JSON, published 2020-06-01T15:15Z)
Description: value: serialize-javascript prior to 3.1.0 allows remote attackers to inject arbitrary code via the function "deleteFunctions" within "index.js".
Severity: MEDIUM (CVSS v3 base score: 8.1)
Weakness: CWE-502 \u2014 Deserialization of Untrusted Data
The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
Repository: https://github.com/yahoo/serialize-javascript
Files changed:
index.js
package-lock.json
package.json
test/unit/serialize.js
Commit message: Don't replace regex / function placeholders within string literals (#79)

Previously we weren't checking if the quote that started the placeholder
was escaped or not, meaning an object like

    {"foo": /1"/, "bar": "a\"@__R-<UID>-0__@"}

Would be serialized as

    {"foo": /1"/, "bar": "a\/1"/}

meaning an attacker could escape out of `bar` if they controlled both
`foo` and `bar` and were able to guess the value of `<UID>`.

UID was generated once on startup, was chosen using `Math.random()` and
had a keyspace of roughly 4 billion, so within the realm of an online
attack.

Here's a simple example that will cause `console.log()` to be called when
the `serialize()`d version is `eval()`d

    eval('('+ serialize({"foo": /1" + console.log(1)/i, "bar": '"@__R-<UID>-0__@'}) + ')');

Where `<UID>` is the guessed `UID`.

This fixes the issue by ensuring that placeholders are not preceded by
a backslash.

We also switch to a higher entropy `UID` to prevent people from guessing it.

Co-authored-by: Jordan Milne <jmilne@uber.com>
Co-authored-by: Ryan Siebert <ryan.siebert@energyaustralia.com.au>


Vulnerable code:
var UID                 = Math.floor(Math.random() * 0x10000000000).toString(16);
var PLACE_HOLDER_REGEXP = new RegExp('"@__(F|R|D|M|S|U|I)-' + UID + '-(\\d+)__@"', 'g');
    return str.replace(PLACE_HOLDER_REGEXP, function (match, type, valueIndex) {
      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
      "dev": true

Fixed code:
var randomBytes = require('randombytes');

var UID_LENGTH          = 16;
var UID                 = generateUID();
var PLACE_HOLDER_REGEXP = new RegExp('(\\\\)?"@__(F|R|D|M|S|U|I)-' + UID + '-(\\d+)__@"', 'g');
function generateUID() {
    var bytes = randomBytes(UID_LENGTH);
    var result = '';
    for(var i=0; i<UID_LENGTH; ++i) {
        result += bytes[i].toString(16);
    }
    return result;
}

    return str.replace(PLACE_HOLDER_REGEXP, function (match, backSlash, type, valueIndex) {
        // The placeholder may not be preceded by a backslash. This is to prevent
        // replacing things like `"a\"@__R-<UID>-0__@"` and thus outputting
        // invalid JS.
        if (backSlash) {
            return match;
        }

    "randombytes": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
      "requires": {
        "safe-buffer": "^5.1.0"
      }
    },
      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
  },
  "dependencies": {
    "randombytes": "^2.1.0"
// temporarily monkeypatch `crypto.randomBytes` so we'll have a
// predictable UID for our tests
var crypto = require('crypto');
var oldRandom = crypto.randomBytes;
crypto.randomBytes = function(len, cb) {
    var buf = Buffer.alloc(len);
    buf.fill(0x00);
    if (cb)
        cb(null, buf);
    return buf;
};

crypto.randomBytes = oldRandom;


    describe('placeholders', function() {
        it('should not be replaced within string literals', function () {
            // Since we made the UID deterministic this should always be the placeholder
            var fakePlaceholder = '"@__R-0000000000000000-0__@';
            var serialized = serialize({bar: /1/i, foo: fakePlaceholder}, {uid: 'foo'});
            var obj = eval('(' + serialized + ')');
            expect(obj).to.be.a('Object');
            expect(obj.foo).to.be.a('String');
            expect(obj.foo).to.equal(fakePlaceholder);
        });
    });