Skip to content

Commit 5009715

Browse files
redonkulusclaude
andauthored
fix: reject spoofed RegExp objects with non-string source property
A fake RegExp created via Object.create(RegExp.prototype) passes instanceof RegExp but can supply an object as .source. That object survives serialize() as executable JS and runs when the consumer evaluates new RegExp(obj, flags) via toString() coercion. Guard mirrors the existing URL fix: assert typeof source === 'string' and throw TypeError otherwise. Fixes: PSECBUGS-112938 Fixes: PSECBUGS-108887 PR-URL: #222 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 153eb43 commit 5009715

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,11 @@ module.exports = function serialize(obj, options) {
259259
if (type === 'R') {
260260
// Sanitize flags to prevent code injection (only allow valid RegExp flag characters)
261261
var flags = String(regexps[valueIndex].flags).replace(/[^gimsuydv]/g, '');
262-
return "new RegExp(" + serialize(regexps[valueIndex].source) + ", \"" + flags + "\")";
262+
var regexpSource = regexps[valueIndex].source;
263+
if (typeof regexpSource !== 'string') {
264+
throw new TypeError('RegExp.source must be a string');
265+
}
266+
return "new RegExp(" + serialize(regexpSource) + ", \"" + flags + "\")";
263267
}
264268

265269
if (type === 'M') {

test/unit/serialize.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,41 @@ describe('serialize( obj )', function () {
316316
strictEqual(serialize(re), 'new RegExp("[\\u003C\\\\\\u002Fscript\\u003E\\u003Cscript\\u003Ealert(\'xss\')\\\\\\u002F\\\\\\u002F]", "")');
317317
});
318318

319+
it('should throw when serializing a spoofed RegExp with non-string source', function () {
320+
var fakeRe = Object.create(RegExp.prototype);
321+
Object.defineProperty(fakeRe, 'source', {
322+
enumerable: true,
323+
value: {
324+
toString: function () {
325+
global.__RE_SOURCE_EXECUTED__ = 'pwned-regexp';
326+
return 'abc';
327+
}
328+
}
329+
});
330+
Object.defineProperty(fakeRe, 'flags', { enumerable: true, value: '' });
331+
throws(function () { serialize({ re: fakeRe }); }, TypeError);
332+
strictEqual(global.__RE_SOURCE_EXECUTED__, undefined);
333+
});
334+
335+
it('should throw when serializing a spoofed RegExp with non-string source via getter', function () {
336+
var fakeRegex = Object.create(RegExp.prototype);
337+
Object.defineProperty(fakeRegex, 'source', {
338+
get: function () {
339+
return {
340+
toString: function () {
341+
global.__REGEXP_SOURCE_GETTER_EXECUTED__ = 'pwned';
342+
return 'x';
343+
}
344+
};
345+
}
346+
});
347+
Object.defineProperty(fakeRegex, 'flags', {
348+
get: function () { return 'g'; }
349+
});
350+
throws(function () { serialize({ re: fakeRegex }); }, TypeError);
351+
strictEqual(global.__REGEXP_SOURCE_GETTER_EXECUTED__, undefined);
352+
});
353+
319354
it('should sanitize RegExp.flags to prevent code injection', function () {
320355
// Object that passes instanceof RegExp with attacker-controlled .flags
321356
var fakeRegex = Object.create(RegExp.prototype);

0 commit comments

Comments
 (0)