Skip to content

Commit 0dd61b8

Browse files
author
Jordan Milne
committed
Don't replace escaped regex / function placeholders in strings
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 is generated once on startup, is chosen using `Math.random()` and has 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.
1 parent adfee60 commit 0dd61b8

File tree

2 files changed

+23
-2
lines changed

2 files changed

+23
-2
lines changed

index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ var isRegExp = require('util').isRegExp;
1010

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

1515
var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g;
1616
var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
@@ -92,7 +92,10 @@ module.exports = function serialize(obj, options) {
9292
// Replaces all occurrences of function and regexp placeholders in the JSON
9393
// string with their string representations. If the original value can not
9494
// be found, then `undefined` is used.
95-
return str.replace(PLACE_HOLDER_REGEXP, function (match, type, valueIndex) {
95+
return str.replace(PLACE_HOLDER_REGEXP, function (match, backSlash, type, valueIndex) {
96+
if (backSlash) {
97+
return match;
98+
}
9699
if (type === 'R') {
97100
return regexps[valueIndex].toString();
98101
}

test/unit/serialize.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
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+
48
var serialize = require('../../'),
59
expect = require('chai').expect;
610

11+
Math.random = oldRandom;
12+
713
describe('serialize( obj )', function () {
814
it('should be a function', function () {
915
expect(serialize).to.be.a('function');
@@ -160,6 +166,18 @@ describe('serialize( obj )', function () {
160166
expect(re).to.be.a('RegExp');
161167
expect(re.source).to.equal('\\..*');
162168
});
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+
});
163181
});
164182

165183
describe('XSS', function () {

0 commit comments

Comments
 (0)