From ae547314dd55f7e76da168ef0d41cad45c294402 Mon Sep 17 00:00:00 2001 From: Seth Date: Wed, 19 Nov 2025 12:18:39 -0800 Subject: [PATCH 1/4] security: escape function bodies --- .gitignore | 2 ++ index.js | 31 ++++++++++++++++++++++++++++--- test/unit/serialize.js | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 3c3629e..d473e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ +.nyc_output/ +coverage/ node_modules diff --git a/index.js b/index.js index e8022a3..63caddc 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,8 @@ See the accompanying LICENSE file for terms. 'use strict'; +var crypto = require('crypto'); + // Generate an internal UID to make the regexp pattern harder to guess. var UID_LENGTH = 16; var UID = generateUID(); @@ -15,6 +17,8 @@ var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g; var IS_PURE_FUNCTION = /function.*?\(/; var IS_ARROW_FUNCTION = /.*?=>.*?/; var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g; +// Regex to match and (case-insensitive) for XSS protection +var SCRIPT_CLOSE_REGEXP = /<\/script>/gi; var RESERVED_SYMBOLS = ['*', 'async']; @@ -32,8 +36,23 @@ function escapeUnsafeChars(unsafeChar) { return ESCAPED_CHARS[unsafeChar]; } +// Escape function body for XSS protection while preserving arrow function syntax +function escapeFunctionBody(str) { + // Escape sequences (case-insensitive) - the main XSS risk + // This must be done first before other replacements + str = str.replace(SCRIPT_CLOSE_REGEXP, function(match) { + return '\\u003C\\u002Fscript\\u003E'; + }); + // Also escape and other case variations + str = str.replace(/<\/SCRIPT>/g, '\\u003C\\u002FSCRIPT\\u003E'); + // Escape line terminators (these are always unsafe) + str = str.replace(/\u2028/g, '\\u2028'); + str = str.replace(/\u2029/g, '\\u2029'); + return str; +} + function generateUID() { - var bytes = crypto.getRandomValues(new Uint8Array(UID_LENGTH)); + var bytes = crypto.randomBytes(UID_LENGTH); var result = ''; for(var i=0; i) while escaping + if (options && options.unsafe !== true) { + serializedFn = escapeFunctionBody(serializedFn); + } + // pure functions, example: {key: function() {}} if(IS_PURE_FUNCTION.test(serializedFn)) { return serializedFn; @@ -261,6 +286,6 @@ module.exports = function serialize(obj, options) { var fn = functions[valueIndex]; - return serializeFunc(fn); + return serializeFunc(fn, options); }); } diff --git a/test/unit/serialize.js b/test/unit/serialize.js index 62c0eee..0e7c2dd 100644 --- a/test/unit/serialize.js +++ b/test/unit/serialize.js @@ -495,6 +495,47 @@ describe('serialize( obj )', function () { strictEqual(serialize(new URL('x:')), 'new URL("x:\\u003C\\u002Fscript\\u003E")'); strictEqual(eval(serialize(new URL('x:'))).href, 'x:'); }); + + it('should encode unsafe HTML chars in function bodies', function () { + function fn() { return ''; } + var serialized = serialize(fn); + strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), true); + strictEqual(serialized.includes(''), false); + // Verify the function still works after deserialization + var deserialized; eval('deserialized = ' + serialized); + strictEqual(typeof deserialized, 'function'); + strictEqual(deserialized(), ''); + }); + + it('should encode unsafe HTML chars in arrow function bodies', function () { + var fn = () => { return ''; }; + var serialized = serialize(fn); + strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), true); + strictEqual(serialized.includes(''), false); + // Verify the function still works after deserialization + var deserialized; eval('deserialized = ' + serialized); + strictEqual(typeof deserialized, 'function'); + strictEqual(deserialized(), ''); + }); + + it('should encode unsafe HTML chars in enhanced literal object methods', function () { + var obj = { + fn() { return ''; } + }; + var serialized = serialize(obj); + strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), true); + strictEqual(serialized.includes(''), false); + // Verify the function still works after deserialization + var deserialized; eval('deserialized = ' + serialized); + strictEqual(deserialized.fn(), ''); + }); + + it('should not escape function bodies when unsafe option is true', function () { + function fn() { return ''; } + var serialized = serialize(fn, {unsafe: true}); + strictEqual(serialized.includes(''), true); + strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), false); + }); }); describe('options', function () { From f9446018d0c52d34401fa4d8694b30f8dac83a2c Mon Sep 17 00:00:00 2001 From: Seth Date: Thu, 20 Nov 2025 07:45:30 -0800 Subject: [PATCH 2/4] revert crypto --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 63caddc..40fda68 100644 --- a/index.js +++ b/index.js @@ -52,7 +52,8 @@ function escapeFunctionBody(str) { } function generateUID() { - var bytes = crypto.randomBytes(UID_LENGTH); + var bytes = crypto.getRandomValues(new Uint8Array(UID_LENGTH)); + crypto.webcrypto.getRandomValues(bytes); var result = ''; for(var i=0; i Date: Thu, 20 Nov 2025 07:47:34 -0800 Subject: [PATCH 3/4] remove --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index d473e0a..3c3629e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ -.nyc_output/ -coverage/ node_modules From 1f322caf594f0f503e6da86f55afca46536ca661 Mon Sep 17 00:00:00 2001 From: Seth Date: Fri, 21 Nov 2025 07:49:46 -0800 Subject: [PATCH 4/4] remove --- index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/index.js b/index.js index 40fda68..b82d679 100644 --- a/index.js +++ b/index.js @@ -53,7 +53,6 @@ function escapeFunctionBody(str) { function generateUID() { var bytes = crypto.getRandomValues(new Uint8Array(UID_LENGTH)); - crypto.webcrypto.getRandomValues(bytes); var result = ''; for(var i=0; i