Skip to content

Commit 58bd2b6

Browse files
author
Jordan Milne
committed
Generate higher-entropy UIDs for placeholders
1 parent 0dd61b8 commit 58bd2b6

File tree

3 files changed

+47
-24
lines changed

3 files changed

+47
-24
lines changed

index.js

+32-6
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ See the accompanying LICENSE file for terms.
66

77
'use strict';
88

9-
var isRegExp = require('util').isRegExp;
9+
var isRegExp = require('util').isRegExp;
10+
var randomBytes = require('randombytes');
1011

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

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

34+
function generateUID() {
35+
var bytes = randomBytes(UID_LENGTH);
36+
var result = '';
37+
for(var i=0; i<UID_LENGTH; ++i) {
38+
result += bytes[i].toString(16);
39+
}
40+
return result;
41+
}
42+
43+
function buildPlaceHolderRegExp(uid) {
44+
return new RegExp('(\\\\)?"@__(F|R)-' + uid + '-(\\d+)__@"', 'g');
45+
}
46+
3247
module.exports = function serialize(obj, options) {
3348
options || (options = {});
3449

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

55+
var uid = options.uid || UID;
4056
var functions = [];
4157
var regexps = [];
4258

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

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

5773
return value;
5874
}
5975

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

6480
return value;
@@ -89,10 +105,20 @@ module.exports = function serialize(obj, options) {
89105
return str;
90106
}
91107

108+
var placeHolderRegExp;
109+
if (options.uid) {
110+
placeHolderRegExp = buildPlaceHolderRegExp(uid);
111+
} else {
112+
placeHolderRegExp = PLACE_HOLDER_REGEXP;
113+
}
114+
92115
// Replaces all occurrences of function and regexp placeholders in the JSON
93116
// string with their string representations. If the original value can not
94117
// be found, then `undefined` is used.
95-
return str.replace(PLACE_HOLDER_REGEXP, function (match, backSlash, type, valueIndex) {
118+
return str.replace(placeHolderRegExp, function (match, backSlash, type, valueIndex) {
119+
// The placeholder may not be preceded by a backslash. This is to prevent
120+
// replacing things like `"a\"@__R-<UID>-0__@"` and thus outputting
121+
// invalid JS.
96122
if (backSlash) {
97123
return match;
98124
}

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,8 @@
3030
"istanbul": "^0.3.2",
3131
"mocha": "^1.21.4",
3232
"xunit-file": "0.0.5"
33+
},
34+
"dependencies": {
35+
"randombytes": "^2.0.3"
3336
}
3437
}

test/unit/serialize.js

+12-18
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
/* global describe, it, beforeEach */
22
'use strict';
33

4-
// Temporarily replace `Math.random` so `UID` will be deterministic
5-
var oldRandom = Math.random;
6-
Math.random = function(){return 0.5};
7-
84
var serialize = require('../../'),
95
expect = require('chai').expect;
106

11-
Math.random = oldRandom;
12-
137
describe('serialize( obj )', function () {
148
it('should be a function', function () {
159
expect(serialize).to.be.a('function');
@@ -117,6 +111,18 @@ describe('serialize( obj )', function () {
117111
});
118112
});
119113

114+
describe('placeholders', function() {
115+
it('should not be replaced within string literals', function () {
116+
// Since we made the UID deterministic this should always be the placeholder
117+
var fakePlaceholder = '"@__R-foo-0__@';
118+
var serialized = serialize({bar: /1/i, foo: fakePlaceholder}, {uid: 'foo'});
119+
var obj = eval('(' + serialized + ')');
120+
expect(obj).to.be.a('Object');
121+
expect(obj.foo).to.be.a('String');
122+
expect(obj.foo).to.equal(fakePlaceholder);
123+
});
124+
});
125+
120126
describe('regexps', function () {
121127
it('should serialize constructed regexps', function () {
122128
var re = new RegExp('asdf');
@@ -166,18 +172,6 @@ describe('serialize( obj )', function () {
166172
expect(re).to.be.a('RegExp');
167173
expect(re.source).to.equal('\\..*');
168174
});
169-
170-
it('should ignore placeholders with leading backslashes', function(){
171-
// Since we made the UID deterministic this should always be the placeholder
172-
var placeholder = '@__R-8000000000-0__@';
173-
var obj = eval('(' + serialize({
174-
"bar": /1/i,
175-
"foo": '"' + placeholder
176-
}) + ')');
177-
expect(obj).to.be.a('Object');
178-
expect(obj.foo).to.be.a('String');
179-
expect(obj.foo).to.equal('"' + placeholder);
180-
});
181175
});
182176

183177
describe('XSS', function () {

0 commit comments

Comments
 (0)