Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.nyc_output/
coverage/
node_modules
31 changes: 28 additions & 3 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');

// 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,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 </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 bytes = crypto.randomBytes(UID_LENGTH);
var result = '';
for(var i=0; i<UID_LENGTH; ++i) {
result += bytes[i].toString(16);
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