From d08f13bd6e709a41d955f99162745005ed098ddc Mon Sep 17 00:00:00 2001 From: redonkulus Date: Mon, 29 Jun 2026 10:38:21 -0700 Subject: [PATCH 1/2] 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 Co-Authored-By: Claude Sonnet 4.6 --- index.js | 6 +++++- test/unit/serialize.js | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index d3fea32..f3db681 100644 --- a/index.js +++ b/index.js @@ -259,7 +259,11 @@ module.exports = function serialize(obj, options) { if (type === 'R') { // Sanitize flags to prevent code injection (only allow valid RegExp flag characters) var flags = String(regexps[valueIndex].flags).replace(/[^gimsuydv]/g, ''); - return "new RegExp(" + serialize(regexps[valueIndex].source) + ", \"" + flags + "\")"; + var regexpSource = regexps[valueIndex].source; + if (typeof regexpSource !== 'string') { + throw new TypeError('RegExp.source must be a string'); + } + return "new RegExp(" + serialize(regexpSource) + ", \"" + flags + "\")"; } if (type === 'M') { diff --git a/test/unit/serialize.js b/test/unit/serialize.js index b4eea83..c276c14 100644 --- a/test/unit/serialize.js +++ b/test/unit/serialize.js @@ -316,6 +316,22 @@ describe('serialize( obj )', function () { strictEqual(serialize(re), 'new RegExp("[\\u003C\\\\\\u002Fscript\\u003E\\u003Cscript\\u003Ealert(\'xss\')\\\\\\u002F\\\\\\u002F]", "")'); }); + it('should throw when serializing a spoofed RegExp with non-string source', function () { + var fakeRe = Object.create(RegExp.prototype); + Object.defineProperty(fakeRe, 'source', { + enumerable: true, + value: { + toString: function () { + global.__RE_SOURCE_EXECUTED__ = 'pwned-regexp'; + return 'abc'; + } + } + }); + Object.defineProperty(fakeRe, 'flags', { enumerable: true, value: '' }); + throws(function () { serialize({ re: fakeRe }); }, TypeError); + strictEqual(global.__RE_SOURCE_EXECUTED__, undefined); + }); + it('should sanitize RegExp.flags to prevent code injection', function () { // Object that passes instanceof RegExp with attacker-controlled .flags var fakeRegex = Object.create(RegExp.prototype); From 72c674a7349e7979dcbc01b74e6b67971042dfff Mon Sep 17 00:00:00 2001 From: redonkulus Date: Mon, 29 Jun 2026 10:44:04 -0700 Subject: [PATCH 2/2] test: cover getter-based RegExp source spoofing (PSECBUGS-108887) Fixes PSECBUGS-108887 Co-Authored-By: Claude Sonnet 4.6 --- test/unit/serialize.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/unit/serialize.js b/test/unit/serialize.js index c276c14..2fc5709 100644 --- a/test/unit/serialize.js +++ b/test/unit/serialize.js @@ -332,6 +332,25 @@ describe('serialize( obj )', function () { strictEqual(global.__RE_SOURCE_EXECUTED__, undefined); }); + it('should throw when serializing a spoofed RegExp with non-string source via getter', function () { + var fakeRegex = Object.create(RegExp.prototype); + Object.defineProperty(fakeRegex, 'source', { + get: function () { + return { + toString: function () { + global.__REGEXP_SOURCE_GETTER_EXECUTED__ = 'pwned'; + return 'x'; + } + }; + } + }); + Object.defineProperty(fakeRegex, 'flags', { + get: function () { return 'g'; } + }); + throws(function () { serialize({ re: fakeRegex }); }, TypeError); + strictEqual(global.__REGEXP_SOURCE_GETTER_EXECUTED__, undefined); + }); + it('should sanitize RegExp.flags to prevent code injection', function () { // Object that passes instanceof RegExp with attacker-controlled .flags var fakeRegex = Object.create(RegExp.prototype);