Skip to content

Commit

Permalink
Generate higher-entropy UIDs for placeholders
Browse files Browse the repository at this point in the history
  • Loading branch information
Jordan Milne committed Oct 15, 2016
1 parent 0dd61b8 commit 58bd2b6
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 24 deletions.
38 changes: 32 additions & 6 deletions index.js
Expand Up @@ -6,11 +6,13 @@ See the accompanying LICENSE file for terms.

'use strict';

var isRegExp = require('util').isRegExp;
var isRegExp = require('util').isRegExp;
var randomBytes = require('randombytes');

// Generate an internal UID to make the regexp pattern harder to guess.
var UID = Math.floor(Math.random() * 0x10000000000).toString(16);
var PLACE_HOLDER_REGEXP = new RegExp('(\\\\)?"@__(F|R)-' + UID + '-(\\d+)__@"', 'g');
var UID_LENGTH = 16;
var UID = generateUID();
var PLACE_HOLDER_REGEXP = buildPlaceHolderRegExp(UID);

var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g;
var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
Expand All @@ -29,6 +31,19 @@ function escapeUnsafeChars(unsafeChar) {
return ESCAPED_CHARS[unsafeChar];
}

function generateUID() {
var bytes = randomBytes(UID_LENGTH);
var result = '';
for(var i=0; i<UID_LENGTH; ++i) {
result += bytes[i].toString(16);
}
return result;
}

function buildPlaceHolderRegExp(uid) {
return new RegExp('(\\\\)?"@__(F|R)-' + uid + '-(\\d+)__@"', 'g');
}

module.exports = function serialize(obj, options) {
options || (options = {});

Expand All @@ -37,6 +52,7 @@ module.exports = function serialize(obj, options) {
options = {space: options};
}

var uid = options.uid || UID;
var functions = [];
var regexps = [];

Expand All @@ -51,14 +67,14 @@ module.exports = function serialize(obj, options) {

if (type === 'object') {
if (isRegExp(value)) {
return '@__R-' + UID + '-' + (regexps.push(value) - 1) + '__@';
return '@__R-' + uid + '-' + (regexps.push(value) - 1) + '__@';
}

return value;
}

if (type === 'function') {
return '@__F-' + UID + '-' + (functions.push(value) - 1) + '__@';
return '@__F-' + uid + '-' + (functions.push(value) - 1) + '__@';
}

return value;
Expand Down Expand Up @@ -89,10 +105,20 @@ module.exports = function serialize(obj, options) {
return str;
}

var placeHolderRegExp;
if (options.uid) {
placeHolderRegExp = buildPlaceHolderRegExp(uid);
} else {
placeHolderRegExp = PLACE_HOLDER_REGEXP;
}

// Replaces all occurrences of function and regexp placeholders in the JSON
// string with their string representations. If the original value can not
// be found, then `undefined` is used.
return str.replace(PLACE_HOLDER_REGEXP, function (match, backSlash, type, valueIndex) {
return str.replace(placeHolderRegExp, 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;
}
Expand Down
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -30,5 +30,8 @@
"istanbul": "^0.3.2",
"mocha": "^1.21.4",
"xunit-file": "0.0.5"
},
"dependencies": {
"randombytes": "^2.0.3"
}
}
30 changes: 12 additions & 18 deletions test/unit/serialize.js
@@ -1,15 +1,9 @@
/* global describe, it, beforeEach */
'use strict';

// Temporarily replace `Math.random` so `UID` will be deterministic
var oldRandom = Math.random;
Math.random = function(){return 0.5};

var serialize = require('../../'),
expect = require('chai').expect;

Math.random = oldRandom;

describe('serialize( obj )', function () {
it('should be a function', function () {
expect(serialize).to.be.a('function');
Expand Down Expand Up @@ -117,6 +111,18 @@ describe('serialize( obj )', function () {
});
});

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-foo-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);
});
});

describe('regexps', function () {
it('should serialize constructed regexps', function () {
var re = new RegExp('asdf');
Expand Down Expand Up @@ -166,18 +172,6 @@ describe('serialize( obj )', function () {
expect(re).to.be.a('RegExp');
expect(re.source).to.equal('\\..*');
});

it('should ignore placeholders with leading backslashes', function(){
// Since we made the UID deterministic this should always be the placeholder
var placeholder = '@__R-8000000000-0__@';
var obj = eval('(' + serialize({
"bar": /1/i,
"foo": '"' + placeholder
}) + ')');
expect(obj).to.be.a('Object');
expect(obj.foo).to.be.a('String');
expect(obj.foo).to.equal('"' + placeholder);
});
});

describe('XSS', function () {
Expand Down

0 comments on commit 58bd2b6

Please sign in to comment.