Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ See the accompanying LICENSE file for terms.

'use strict';

var crypto = require('crypto');

Comment on lines +9 to +10
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node.js LTS versions should work even without loading the crypto module.

Suggested change
var crypto = require('crypto');

// Generate an internal UID to make the regexp pattern harder to guess.
var UID_LENGTH = 16;
var UID = generateUID();
Expand All @@ -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 </script> and </SCRIPT> (case-insensitive) for XSS protection
var SCRIPT_CLOSE_REGEXP = /<\/script>/gi;

var RESERVED_SYMBOLS = ['*', 'async'];

Expand All @@ -32,6 +36,21 @@ function escapeUnsafeChars(unsafeChar) {
return ESCAPED_CHARS[unsafeChar];
}

// Escape function body for XSS protection while preserving arrow function syntax
function escapeFunctionBody(str) {
// Escape </script> 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 </SCRIPT> 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 result = '';
Expand Down Expand Up @@ -138,12 +157,18 @@ module.exports = function serialize(obj, options) {
return value;
}

function serializeFunc(fn) {
function serializeFunc(fn, options) {
var serializedFn = fn.toString();
if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) {
throw new TypeError('Serializing native function: ' + fn.name);
}

// Escape unsafe HTML characters in function body for XSS protection
// This must preserve arrow function syntax (=>) while escaping </script>
if (options && options.unsafe !== true) {
serializedFn = escapeFunctionBody(serializedFn);
}

// pure functions, example: {key: function() {}}
if(IS_PURE_FUNCTION.test(serializedFn)) {
return serializedFn;
Expand Down Expand Up @@ -261,6 +286,6 @@ module.exports = function serialize(obj, options) {

var fn = functions[valueIndex];

return serializeFunc(fn);
return serializeFunc(fn, options);
});
}
41 changes: 41 additions & 0 deletions test/unit/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,47 @@ describe('serialize( obj )', function () {
strictEqual(serialize(new URL('x:</script>')), 'new URL("x:\\u003C\\u002Fscript\\u003E")');
strictEqual(eval(serialize(new URL('x:</script>'))).href, 'x:</script>');
});

it('should encode unsafe HTML chars in function bodies', function () {
function fn() { return '</script>'; }
var serialized = serialize(fn);
strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), true);
strictEqual(serialized.includes('</script>'), false);
// Verify the function still works after deserialization
var deserialized; eval('deserialized = ' + serialized);
strictEqual(typeof deserialized, 'function');
strictEqual(deserialized(), '</script>');
});

it('should encode unsafe HTML chars in arrow function bodies', function () {
var fn = () => { return '</script>'; };
var serialized = serialize(fn);
strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), true);
strictEqual(serialized.includes('</script>'), false);
// Verify the function still works after deserialization
var deserialized; eval('deserialized = ' + serialized);
strictEqual(typeof deserialized, 'function');
strictEqual(deserialized(), '</script>');
});

it('should encode unsafe HTML chars in enhanced literal object methods', function () {
var obj = {
fn() { return '</script>'; }
};
var serialized = serialize(obj);
strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), true);
strictEqual(serialized.includes('</script>'), false);
// Verify the function still works after deserialization
var deserialized; eval('deserialized = ' + serialized);
strictEqual(deserialized.fn(), '</script>');
});

it('should not escape function bodies when unsafe option is true', function () {
function fn() { return '</script>'; }
var serialized = serialize(fn, {unsafe: true});
strictEqual(serialized.includes('</script>'), true);
strictEqual(serialized.includes('\\u003C\\u002Fscript\\u003E'), false);
});
});

describe('options', function () {
Expand Down